JSON Decode Fails at the First Character: Check the HTTP Body Contract
By ON Lab · Sources checked November 8, 2026
A JSON decoding error at the first character can mean an empty body or a body that is not JSON. It does not identify the cause by itself. Inspect the HTTP status, actual Content-Type header and body length separately. A 204 response has no content to decode; a 200 response is not a guarantee that its body is valid JSON.
Compare four controlled responses
The historical Requests question concerned calling json on an empty response. We reproduced the response boundary with a local Python 3.13.18 server and standard-library client, without installing Requests or contacting an external API.
| Local route | Status/header | Body | Direct JSON decoding |
|---|---|---|---|
| JSON control | 200, application/json | {"ok":true} | Parsed successfully |
| HTML control | 200, text/html | HTML text | Failed |
| No-content control | 204, no Content-Type | Zero bytes | Failed |
| Wrong-body control | 200, application/json | not JSON | Failed |
The header is useful evidence, not proof of valid syntax. Conversely, an empty no-content response can be a valid protocol outcome even though a JSON parser rejects its empty input.
Choose a response contract before parsing
This example distinguishes an intentional no-content result from a parsed JSON value. It does not fabricate an empty dictionary for an absent body.
import json
def read_result(status, content_type, body):
if status == 204:
if body:
raise ValueError("Unexpected content on a 204 response")
return ("no_content", None)
if not body:
raise ValueError("Empty body; check the endpoint contract")
media_type = (content_type or "").split(";", 1)[0].strip().lower()
if media_type != "application/json" and not media_type.endswith("+json"):
raise ValueError("Expected a JSON media type")
return ("json", json.loads(body))
print(read_result(200, "application/json", b'{"ok":true}'))
print(read_result(204, None, b""))The example returns different tagged results. Your application must still decide whether a no-content result is allowed for that operation and how to handle unsuccessful HTTP statuses. Do not apply this function as a complete HTTP client or schema validator.
Look at the bytes without exposing private data
For a harmless test response, record status, the raw Content-Type header, body length and a short escaped prefix. For a real endpoint, avoid logging tokens, cookies, personal records or an entire private HTML page. Some header helper methods supply a default media type when a header is absent; distinguish that default from a header the server actually sent.
If the prefix is HTML, inspect the final response URL, redirects and endpoint documentation. A login page, site error page or wrong route is possible, but the character < alone does not prove which one occurred. Do not repeatedly authenticate or replay a consequential request just because decoding failed.
An empty object is a different value
{}, null, an empty JSON string and a zero-byte HTTP body are different inputs. A fallback that returns {} for every decoder error hides malformed content and can make a failed response look like successful empty data. Only adopt such a mapping when the endpoint’s contract explicitly calls for it.
The same separation matters for status checks. A successful transfer is not proof that the application accepted an action. Check that outcome through the documented response contract; this local test did not send a message, modify an account or perform a business transaction.
If the header says JSON but decoding still fails
- Keep the status and exact failure separate from the body’s claimed type.
- Confirm that the body is nonempty and not truncated.
- Inspect a non-sensitive synthetic reproduction of its actual syntax.
- Distinguish one malformed value from multiple concatenated values.
- Report the failing boundary rather than replacing it with guessed data.
The JSON framing guide covers concatenated values. The nonfinite-number guide covers a different interoperability problem. Neither is a reason to parse an empty 204 body.
Method: current Python and HTTP 204 documentation, a historical user question and four loopback-only response fixtures. The final 204 fixture omitted both a body and Content-Length. No live API, current Requests implementation, authentication flow or complete HTTP/schema handling was tested.