Describe the bug
When the token endpoint returns a non-JSON body with an HTTP status code below 500, OAuth2Client.fetch_token() raises a json.decoder.JSONDecodeError instead of a meaningful error that can be retried.
This happens because parse_response_token only calls resp.raise_for_status() when resp.status_code >= 500.
For any status code below 500 this check is skipped and if the response body isn't valid JSON, the code falls straight into resp.json() and raises with an unhandled JSONDecodeError. 429 is the case I hit in practice (auth servers/gateways send plain-text or HTML bodies on rate-limit responses), but the underlying issue applies to any non-5xx status paired with a non-JSON body.
Error Stacks
Traceback (most recent call last):
File "/mnt/c/Users/Michael/Downloads/test.py", line 8, in <module>
token = client.fetch_token(
url=token_endpoint,
grant_type="client_credentials",
)
File "/home/michael/.local/lib/python3.13/site-packages/authlib/oauth2/client.py", line 246, in fetch_token
return self._fetch_token(
~~~~~~~~~~~~~~~~~^
url, body=body, auth=auth, method=method, headers=headers, **session_kwargs
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/home/michael/.local/lib/python3.13/site-packages/authlib/oauth2/client.py", line 445, in _fetch_token
return self.parse_response_token(resp)
~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
File "/home/michael/.local/lib/python3.13/site-packages/authlib/oauth2/client.py", line 418, in parse_response_token
token = resp.json()
File "/home/michael/.local/lib/python3.13/site-packages/httpx/_models.py", line 832, in json
return jsonlib.loads(self.content, **kwargs)
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
~~~~~~~~~~~~~~~~~~~~~~~^^^
File "/usr/lib/python3.13/json/decoder.py", line 348, in decode
raise JSONDecodeError("Extra data", s, end)
json.decoder.JSONDecodeError: Extra data: line 1 column 5 (char 4)
To Reproduce
server.py:
from http.server import HTTPServer, BaseHTTPRequestHandler
class TooManyRequestsHandler(BaseHTTPRequestHandler):
def handle_request(self):
self.send_response(429)
self.end_headers()
self.wfile.write(b"429 Too Many Requests")
def do_GET(self):
self.handle_request()
def do_POST(self):
self.handle_request()
def do_PUT(self):
self.handle_request()
def do_DELETE(self):
self.handle_request()
def do_PATCH(self):
self.handle_request()
if __name__ == "__main__":
server = HTTPServer(("localhost", 8000), TooManyRequestsHandler)
print("Server running on http://localhost:8000 (all routes return 429)")
server.serve_forever()
test.py:
from authlib.integrations.httpx_client import OAuth2Client
client_id = "your_client_id"
client_secret = "your_client_secret"
token_endpoint = "http://localhost:8000"
with OAuth2Client(client_id, client_secret) as client:
token = client.fetch_token(
url=token_endpoint,
grant_type="client_credentials",
)
print(token)
Expected behavior
A clear, catchable error is raised, either httpx.HTTPStatusError via resp.raise_for_status() or an authlib specific error.
Environment:
- OS: any
- Python Version: 3.13.5
- Authlib Version: 1.7.2 but any will do
Additional context
Describe the bug
When the token endpoint returns a non-JSON body with an HTTP status code below 500,
OAuth2Client.fetch_token()raises ajson.decoder.JSONDecodeErrorinstead of a meaningful error that can be retried.This happens because parse_response_token only calls
resp.raise_for_status()whenresp.status_code >= 500.For any status code below 500 this check is skipped and if the response body isn't valid JSON, the code falls straight into
resp.json()and raises with an unhandledJSONDecodeError. 429 is the case I hit in practice (auth servers/gateways send plain-text or HTML bodies on rate-limit responses), but the underlying issue applies to any non-5xx status paired with a non-JSON body.Error Stacks
To Reproduce
server.py:test.py:Expected behavior
A clear, catchable error is raised, either
httpx.HTTPStatusErrorviaresp.raise_for_status()or an authlib specific error.Environment:
Additional context