Enforce JSON Request Byte Limits with TextEncoder
A profile update fits a client-side length check but is rejected by an API’s body-size limit. The check counted JavaScript string units, while the endpoint limits UTF-8 bytes. Accented text, emoji, and JSON escaping can make those measurements differ.
For a small JSON endpoint, serialize the complete value once, encode that text, and compare the resulting byte count with the endpoint’s documented limit. Then send those same bytes. The examples use standard JavaScript APIs available in modern browsers and Node.js.
Count UTF-8 Bytes Instead of Code Units
JavaScript’s length counts UTF-16 code units. It does not measure the UTF-8 representation used by a JSON request:
| |
The Unicode escapes keep the source easy to copy; JavaScript evaluates them into actual characters. The
String length documentation
explains the code-unit count. The
WHATWG Encoding Standard
specifies that TextEncoder produces UTF-8 in a Uint8Array.
Counting code points with [...text].length also does not give a byte count. Use the encoded representation when the limit is expressed in bytes.
Serialize the Whole JSON Body Once
Measuring only a nickname misses property names, quotes, braces, and escaped control characters. Save this helper as json-body.mjs:
| |
An exact match is accepted; one byte over the limit is rejected. The returned array contains the entire compact JSON document, including its syntax. No whitespace formatting or replacement rules are added.
Serialize only once because getters and toJSON() methods can run during serialization. Repeating JSON.stringify(value) for the request could produce different text from the text you measured. Reusing the encoded body avoids that mismatch.
This helper allocates both the JSON text and its encoded bytes before checking the limit. It suits bounded application data such as a profile update. It does not bound memory consumption while serializing a huge object.
Send the Validated Bytes with Fetch
Import the helper into a module and pass its returned array directly to fetch:
| |
Call this with your application’s endpoint, payload, and documented byte limit; catch the returned promise’s rejection in the calling UI. Validation finishes before fetch is invoked. A failed size check therefore prevents this function from starting a request.
The Fetch Standard’s body extraction rules accept a buffer source and copy its bytes. Set the JSON content type explicitly because a typed array does not identify its contents as JSON. The caller can parse the returned response according to the endpoint’s contract, including handling an empty response body.
The byte count covers this uncompressed JSON body. It excludes headers and transport framing. Confirm whether the service’s limit applies to these body bytes, a compressed representation, or a decoded field. The server still needs to enforce its own limits. An HTTP rejection also needs the response.ok check shown above; the
Fetch guide
describes that behavior.
Keep Serialization Failures Visible
The helper preserves the normal behavior of JSON.stringify:
| Input | Result |
|---|---|
| A serializable object, array, or primitive | Encode the resulting JSON text |
Top-level undefined, function, or symbol | Reject because no JSON text was produced |
A circular reference or an unhandled BigInt | Propagate the serialization error |
An object property whose value is undefined | Omit the property under normal JSON rules |
A throwing getter or toJSON() method | Propagate that error |
These serialization rules are documented in JSON.stringify . The helper checks size, not whether the resulting document has the fields your endpoint requires. Validate that schema separately.
Do not replace a serialization error with an empty object merely to make the request fit. Report it at the call site so the application can correct its payload. Likewise, avoid slicing the encoded byte array to the maximum length: the cut can break a UTF-8 sequence or leave incomplete JSON. Reduce optional fields or split a batch using a contract the server supports, then serialize and measure again.
Test Exact Byte Boundaries
Save this test beside json-body.mjs as json-body.test.mjs, then run node json-body.test.mjs:
| |
The first case includes both an accented letter and an emoji, and verifies that decoding the measured body preserves the payload. The newline case proves that JSON escaping is included in the budget.
Also test the request wrapper with a mocked fetch: an oversized payload must make zero calls, while an accepted payload must send the validated bytes with the JSON content type. Finally, test the actual endpoint at its documented boundary to confirm that the client and server measure the same representation.