httpx JSON decode error: response.json() fails (tested)

r.json() raises json.JSONDecodeError when the body is empty, an HTML error page, or several JSON lines. Check r.status_code and r.text[:200]; catch json.JSONDecodeError (or ValueError), not httpx.HTTPError.

response.json() in httpx parses the body with the standard json module, so a body that is not JSON raises json.JSONDecodeError (a ValueError), not an httpx exception. Measured with httpx 0.28.1 on Python 3.13.5; srv is a local http.server returning the bodies named by each path.

1. The three usual causes

import json, httpx, srv
url = srv.start()
for path in ["/empty", "/html", "/ndjson"]:
    r = httpx.get(url + path)
    try:
        r.json()
    except json.JSONDecodeError as e:
        print(f"{path}: {r.status_code} {type(e).__module__}.{type(e).__name__}: {e}")
print("is it an httpx exception?", issubclass(json.JSONDecodeError, httpx.HTTPError))

Output:

/empty: 204 json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
/html: 502 json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
/ndjson: 200 json.decoder.JSONDecodeError: Extra data: line 2 column 1 (char 9)
is it an httpx exception? False

An empty body (204, or an empty 200) and an HTML error page both give Expecting value: line 1 column 1 (char 0); print r.status_code and r.text[:200] to see which. Newline-delimited JSON gives Extra data. except httpx.HTTPError does not catch any of them.

2. Content-Type is not checked

import httpx, srv
url = srv.start()
r = httpx.get(url + "/ok")
print(r.headers["content-type"], "->", r.json())
r = httpx.get(url + "/bom")
print("UTF-8 BOM ->", r.json())

Output:

text/plain -> {'id': 1}
UTF-8 BOM -> {'id': 1}

r.json() parsed a text/plain body without complaint, and handled a UTF-8 byte-order mark. So the error is always about the bytes, never the header.

3. A safe get_json helper

import json, httpx, srv
url = srv.start()

def get_json(client, path):
    r = client.get(path)
    r.raise_for_status()
    if r.status_code == 204 or not r.content:
        return None
    if "json" not in r.headers.get("content-type", ""):
        raise ValueError(f"expected JSON, got {r.headers.get('content-type')}: {r.text[:60]!r}")
    return r.json()

with httpx.Client(base_url=url) as c:
    print("/empty ->", get_json(c, "/empty"))
    try:
        get_json(c, "/html")
    except httpx.HTTPStatusError as e:
        print("/html ->", e.response.status_code, e.response.text[:40])

Output:

/empty -> None
/html -> 502 <html><body>Bad Gateway</body></html>

Check the status first (raise_for_status() turns the 502 HTML page into an HTTPStatusError with the body attached), return None for empty bodies, and fail with a readable message if the server sent something other than JSON.

4. Streaming NDJSON / JSON lines

import json, httpx, srv
url = srv.start()
with httpx.Client() as c:
    with c.stream("GET", url + "/ndjson") as r:
        for line in r.iter_lines():
            if line:
                print(json.loads(line))

Output:

{'a': 1}
{'a': 2}

For one JSON object per line, decode each line yourself with iter_lines() instead of calling r.json() on the whole body.

Related: JSONDecodeError: Expecting value line 1 column 1, JSONDecodeError: Extra data, httpx raise_for_status, httpx streaming, httpx vs requests differences.

Frequently asked questions

What exception does httpx response.json() raise?

json.JSONDecodeError from the standard library, a subclass of ValueError. It is not an httpx.HTTPError.

Why does httpx json() say Expecting value: line 1 column 1 (char 0)?

The body is empty (e.g. 204) or not JSON at all (e.g. an HTML error page). Inspect r.status_code and r.text.

Does httpx check the Content-Type before json()?

No. In the test it parsed a text/plain body as JSON.

Written with AI assistance. Measured on 2026-10-23 with httpx 0.28.1 on Python 3.13.5 against a local http.server. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; httpx is free.