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.