Watch at EaseStories. Answers. Perspective.

technology · PRACTICAL GUIDE

requests JSONDecodeError at char 0? Prove the response bytes before parsing

Capture the final URL, status, Content-Type, raw byte length and a safe prefix; then distinguish a legitimate empty response, HTML or text, a consumed stream, and an actual JSON document.

Original diagram tracing method, final URL, status, Content-Type, raw bytes and the JSON parse gate.
Original illustration: preserve the HTTP response before calling json(). Watch at Ease

“Expecting value: line 1 column 1 (char 0)” means the decoder found no JSON value at the first parse position. It does not prove bad quotes, a broken JSON library or even that the server promised JSON.

Preserve one response before retrying: method, final URL, redirects, status, Content-Type, raw byte length and a redacted prefix. That record points to the HTTP or stream boundary that must be repaired.

Freeze one failing exchange before retrying

Log the request method, sanitized target, response.status_code, response.url and redirect history from the same failing call. Preserve timing and a correlation ID when available. Repeated calls can land on another backend or refresh an expiring login, destroying the failure you need to explain.

Do not log credentials, cookies, authorization headers or an unbounded body. Record header names selectively and use a short, redacted prefix. The goal is reproducible evidence, not a secret-bearing packet capture.

Measure bytes before choosing a parser

Read response.content once into a bounded variable and record its byte length. A zero length immediately separates “there is no JSON document” from malformed JSON. If stream=True was used, establish which component owns consumption; iter_content(), raw.read() or middleware may already have advanced the stream.

Keep the original bytes for diagnosis. Decoding response.text can involve a guessed character encoding, while quote replacement or strip operations mutate evidence. A safe prefix such as repr(body[:160]) can expose HTML, plain text, a proxy banner or a byte-order mark without pretending it is JSON.

Interpret status, redirects and Content-Type together

Requests documents that 204 No Content or an invalid body makes response.json() raise JSONDecodeError. A 204 can be a successful API outcome, so the correct branch may be “return no object” rather than “repair JSON.” Apply the endpoint contract, not a blanket 200-only rule.

Inspect the final URL and redirect history. A login page, consent page, rate-limit response, 404 HTML or 502 gateway document often begins with “<” and fails at char zero. Content-Type such as text/html or text/plain supports that diagnosis, but still compare it with actual bytes and the API contract.

Separate HTTP failure from representation failure

Choose whether raise_for_status() belongs before parsing according to the API contract. Some APIs return useful JSON error objects with 4xx or 5xx status, while others return HTML. Requests explicitly warns that JSON-decoding success alone does not indicate HTTP success.

If the status is unexpected, fix authentication, endpoint, redirect, proxy or upstream health first. If the status is expected but the media type or bytes are not, treat that as a representation-contract violation and preserve a small sample plus response identifiers for the service owner.

Parse once, validate shape and prove the exit condition

Only after the contract permits JSON and non-empty bytes are present should the application call response.json() or json.loads(body). Catch requests.exceptions.JSONDecodeError at the boundary and attach the safe evidence record; do not silently substitute an empty dictionary because that converts transport failure into false business data.

Close the incident by repeating the same method and endpoint through the same runtime and network path. Save an expected status, JSON-compatible Content-Type, non-zero bytes, one successful parse and schema validation. This remains distinct from “Extra data,” where a valid first JSON value exists but trailing records need framing.

Original decision tree separating empty, non-JSON and valid JSON response candidates.
Original illustration: branch on raw response bytes, then repair the matching boundary. Watch at Ease