Huny's Dev Blog
huny.dev

HTTP Content-Length for UTF-8 JSON: Count Bytes, Not Characters

Fix truncated UTF-8 JSON requests by measuring body bytes. Six local Python HTTP checks compare automatic Content-Length, explicit lengths, Korean text, emoji, and ASCII escaping.

Hun Jang
Hun Jang Nov 5, 2026

If JSON requests fail or appear truncated only when they contain Korean text, check whether HTTP Content-Length was calculated from the string's character count. Build the actual UTF-8 body bytes first and use their length. With Python http.client, passing a byte body without manually setting the length header lets the library calculate it. The local HTTP example below verifies both the failure and the fix.

Concept illustration of a group of characters becoming a longer sequence of bytes, with measuring rails. It does not depict exact character or byte counts.

Written and documentation checked: November 5, 2026. Verified on Linux with CPython 3.12.14, a standard-library HTTP client, and a loopback server. The requests contain uncompressed JSON. This is not a claimed reproduction of an incident in a real TTS service or external API.

The character count can be right while the HTTP length is wrong

RFC 9110 §8.6 defines Content-Length in octets: bytes. In this example, count the entire serialized body, including braces, keys, and quotation marks, rather than just the characters inside a JSON value.

The example {"text":"안녕"} has a Python string length of 13 but occupies 17 UTF-8 bytes. ASCII-only input gives equal lengths and can hide the incorrect calculation. Python's len(str) is not always a count of visually perceived characters either, so do not treat it as a general transmission-length calculation.

RFC 8259 §8.1 requires UTF-8 for JSON exchanged between systems outside a closed ecosystem. Choosing the encoding and calculating the length are related: finish encoding before counting the bytes that will actually be sent.

Fix the sequence: serialize → encode as UTF-8 → send

The successful path creates the body once with json.dumps(...).encode("utf-8") and sends that byte object. It sets Content-Type: application/json and omits Content-Length.

The Python 3.12 HTTPConnection.request documentation describes automatic length-header generation for a byte body when neither Content-Length nor Transfer-Encoding is supplied. This describes that API and body type; it does not imply that every HTTP library or streaming input behaves identically.

If a low-level path requires an explicit header, obtain the length from the actual byte object to be sent, as in len(body). Do not serialize again with different whitespace or escaping settings, or change the body after measuring it.

Runnable reproduction: the wrong 13 bytes and the correct 17

Save the following as content_length_demo.py and run python3 content_length_demo.py. No external packages are required. The server binds only to an ephemeral port on 127.0.0.1 and closes on completion.

This server is a test fixture that reads the declared length and reports whether the result parses as JSON. It is not a production HTTP server implementation or guidance for handling malformed requests. Each failure experiment closes its connection so leftover bytes are not reused as another request.

Actual output:

Interpret the results separately

Check
Observed result
Meaning
ASCII JSON
16 characters, 16 bytes; automatic length works
ASCII-only tests can hide the bug
Korean JSON
13 characters, 17 bytes; automatic length works
Use the UTF-8 length of the entire body
Emoji JSON
12 characters, 15 bytes; automatic length works
A language-based multiplier is insufficient
Explicit character count of 13
Reads 13 of 17 bytes; JSON parsing fails
Check body length before focusing on decoding or parsing
Explicit byte count of 17
Restores the original Korean value
Measure the same byte body that is sent
ASCII-escaped JSON
23 bytes; decoded value is unchanged
Identical JSON values can have different serialized sizes

The final check uses a separate representation produced with ensure_ascii=True. Changing this setting can mask the header bug, but cannot replace correct length handling. The original value is unchanged while its transmitted representation differs.

HTTP 200 in the diagnostic response means the test server returned its report. It does not mean a request reported as valid=false contained valid JSON. Do not assume a production API returns the same status or error message.

Scope and verification steps

This is a local experiment with HTTP/1.1 requests and HTTP/1.0 diagnostic responses. It does not test proxies, TLS, HTTP/2, HTTP/3, chunked transfer, or compressed bodies. Do not add a guessed length to a valid streaming request that has no Content-Length. Follow the client documentation for its transfer mode.

For production diagnosis, first compare the body's byte length immediately before transmission with the length header, then test ASCII, Korean, and emoji inputs separately. Start by logging necessary information such as lengths and request IDs rather than full content. If the request still fails with library-managed headers, inspect post-serialization body changes, intermediary handling, and the server's actual reading and parsing stages separately.

Do not send malformed Content-Length experiments to public services. This experiment verifies body truncation on a controlled single connection. Request boundaries and security behavior across real intermediaries require separate verification. RFC 9110 §8.6 on incorrect lengths

한국어 버전: 한글 JSON 요청이 잘릴 때: HTTP Content-Length는 문자 수가 아니라 바이트 수다한글 JSON 요청이 잘릴 때: HTTP Content-Length는 문자 수가 아니라 바이트 수다

추천 글