Desk Clarity

Everyday document & browser help

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 routeStatus/headerBodyDirect JSON decoding
JSON control200, application/json{"ok":true}Parsed successfully
HTML control200, text/htmlHTML textFailed
No-content control204, no Content-TypeZero bytesFailed
Wrong-body control200, application/jsonnot JSONFailed

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

  1. Keep the status and exact failure separate from the body’s claimed type.
  2. Confirm that the body is nonempty and not truncated.
  3. Inspect a non-sensitive synthetic reproduction of its actual syntax.
  4. Distinguish one malformed value from multiple concatenated values.
  5. 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.

Sources