Repository navigation
Should error responses raise an HTTPError by default? #752
Description
I raised this question on the chat, but I think it's worth a proper issue to discuss options…
Current situation and pain points
Currently, to be notified of an error response we need to either:
- Check the
.status_codemanually:
if response.status_code == 404:
print("Not found")
if response.is_error:
print("Error")- Call
.raise_for_status():
response.raise_for_status() # Raises an HTTPErrorThis is mostly in line with what Requests does, but I'd argue we can do better.
In particular, there's a very basic use case we could address better: make a request, then handle any 4xx/5xx.
There are mostly two ways to do this currently:
- Inspect the
.status_code:
import httpx
r = httpx.get("https://httpbin.org/status/500")
if r.status_code == 500:
...- Call
.raise_for_status()to get a properHTTPError:
import httpx
def fetch(url):
r = httpx.get(url)
r.raise_for_status()
return r
try:
r = fetch("https://httpbin.org/status/500")
except httpx.HTTPError as exc:
if exc.response.status_code == 500:
...I think we can argue this looks somewhat clunky. It forces the user to add a .raise_for_status() line basically everywhere — probably as a wrapper around an HTTPX call — otherwise they'd start processing responses they think are valid, when they're actually errors.
Possible future situation
What if request methods automatically raised an HTTPError if we get an error response? Sounds sensible?
import httpx
try:
r = httpx.get("https://httpbin.org/status/500")
except httpx.HTTPError as exc:
print(exc.response.status_code)
# Deal with 500...This would allow users to have to think about error handling, as they'd be getting HTTPError exceptions thrown at them when hosts aren't able to satisfy the request. We can also easily add a "manual mode" without breaking backwards compat after 1.0…
Planning
If we think this is reasonable, the way I'd see us implement this is:
- Switch to raise an error as the default behavior before 1.0.
- This would be done via a
raise_for_status=Trueflag, probably on the client constructor at first. - Switch the docs to document the new default behavior, and document
raise_for_status=False+response.raise_for_status/response.is_erroras a more low-level/manual way of handling things. - Later on (i.e. after 1.0), we could extend this to allow a
Callable[[int], bool]too, allowing to do things likeraise_for_status=lambda s: 500 <= s < 600so that 4xx error responses don't raise anHTTPError. (Sounds fancy, but I can see this feature request turning up at some point.)
Activity
[-]Error responses should raise an HTTPError by default[/-][+]Should error responses raise an HTTPError by default?[/+]StephenBrown2 commentedon Jan 12, 2020
I was only halfway on board, until you mentioned being able to pass a
Callable. Now that sold me! (Thinking of the potential for retry functionality...)florimondmanca commentedon Jan 12, 2020
I don’t think this is a critical point for implementing retries, is it? Checking for error responses would happen at the outermost layer of the client (in .send()), while I assume retries would be implement below that (similar to redirects and auth). Retries can (should?) check the status code manually — I mostly see this issue as a UX improvement, really.
florimondmanca commentedon Jan 12, 2020
The part I’m not too sure about yet is where to put the
raise_for_statusflag.Although this issue mentions putting it on the client, #754 puts it on request methods. That’s because I think it makes sense to be able to override the default behavior on a per-request basis, instead of having to build another client just because eg a specific endpoint returns a 403 you’d like to handle more manually.
But defining the behavior across a client also makes sense I suppose (especially to disable automatic raising of responses, eg if we porting from Requests).
So I guess the request level could be treated as an incremental improvement, and I should maybe update #754 to add a client-level flag first?
To be honest though, I’m only halfway convinced we should keep (read: advertise) a manual API at all...
There’s nothing we can do with a manual inspection of status codes and
.raise_for_statusthat we can’t do with an API that raises anHTTPErrorcontaining a.responseattribute. We can still inspect the status code and act accordingly, ie handle the exception or re-raise it.The only benefit I can see of keeping the manual API available by the reach of a flag is to help with an incremental migration from Requests. But even then our manual API already isn’t 100% compatible (eg we don’t have
.is_ok).This doesn’t mean we won’t need a parameter at all in the future, if only to customize what it means for a response to be an error. But I think this can also be addressed via proper exception handling on the user side. After all, semantically a 403 doesn’t cease to be an error response just because you plan to maybe handle it somewhere in your error handling code. (What if you don’t/forget to?)
I think we could get away with no flag/callable parameter at all, for the sake of keeping the scope tight.
lovelydinosaur commentedon Jan 13, 2020
I'm not sure about this. My initial reaction would be that it feels a bit blunt, and that exceptions should default to network failures etc. only.
Not absolutely against it, but it'd be a huge change in behaviour over requests, and we'd need to think long and hard about it.
Starting with a different question here:
If we wanted to introduce an optional flag on the client to auto raise on
4xx,5xx, then:Client(raise_on_status=True)?Client(auto_raise=True)?.get(..., raise_on_status=False),.get(..., raise_on_status=True)?(I'm not absolutely convinced about that one either, but it's a smaller point of design discussion to start with.)
thebigmunch commentedon Jan 13, 2020
Same.
I always wondered why it wasn't named
raise_on_errorrather thanraise_on_status. I could also seeraise_if_error. Though I'm not sure HTTPX should deviate from requests with the naming if it's going to make this big of a behavior change.StephenBrown2 commentedon Jan 13, 2020
It's not critical, unless the Callable was handed the request and response object (which the exception already contains:
an API that raises an HTTPError containing a .response attribute.), in which case the callable could retry or not, just removing atry/exceptblock really.Seconded.
Also seconded.
raise_for_statusmeans... what exactly? Especially since I can't specify the status code(s) to raise on if I wanted to: https://requests.readthedocs.io/en/master/api/#requests.Response.raise_for_status only raises storedHTTPError, if one occurred, which means that it could/should be folded into theClient/requesteasily asraise_on_error.florimondmanca commentedon Jan 13, 2020
Okay, I'd agree with down-sizing this a bit. Let's switch from "raise errors on error HTTP responses by default" to "add an optional flag to raise errors by default".
Options (showing the default value):
raise_for_status: in line withresponse.raise_for_status(). Probably the most sane option unless we decide to change the response method too.raise_on_status: slightly more meaningful IMO.raise_on_error: not a good option IMO, because it might lead to thinking that passingFalsewould not raise any error (which isn't true: it would still raiseNetworkError, timeout exceptions, etc).So, I guess the most sane option is
raise_for_status. It would be disabled by default, and we could enabled it using…Also, another data point:
aiohttp.ClientSession()provides araise_for_status=Falseflag.@StephenBrown2 About retries: I still don't think it's necessarily related to this issue. Any auto-raise-on-status functionality would happen right at the edge of the
Client.send(), and retries would probably be implemented as an inner method. Something like…As long as we guarantee that an
HTTPErrorraised in.send_handling_retries()corresponds an actual error while trying to get a response (as above), I really think retries and auto-raise-on-error-responses can be treated as separate, orthogonal features.florimondmanca commentedon Jan 13, 2020
Note that if we're going for an off-by-default optional flag (instead of on-by-default), it also means we can treat this as a possible post-1.0 enhancement…
hugovk commentedon Jan 13, 2020
It would be good for migration too. For example, change this:
Into:
Or:
StephenBrown2 commentedon Jan 13, 2020
Another, slightly more verbose option:
raise_on_error_statusSeems entirely clear to me that that's only additionally raising on error statuses in the response, rather than any network error.
Definitely would like to see it as an "off/
False-by-default" option.Agreed. 👍
florimondmanca commentedon Jan 14, 2020
Yes, agreed. But… that's diverging from the
.raise_for_status()naming we have on the method, which we inherit from Requests.I'd be fine with switching to
.raise_on_error_status()/raise_on_error_status=True, but that would need to be documented as a difference with Requests, and maybe even have some special runtime fool-proofing. ("You're trying to use.raise_for_status()— it's.raise_on_error_status()here.")StephenBrown2 commentedon Jan 14, 2020
I might leave
response.raise_for_status()for requests compatibility, but the flag would beraise_on_error_status=, since it's a diversion from requests' interface, no need to remain compliant.thebigmunch commentedon Jan 14, 2020
I'd say they should both be the same if a flag is added. It just adds unnecessary confusion otherwise; things are usually named differently because they do at least slightly different things. If HTTPX deviates from requests in this at all, it should probably be to raise without bothering with a flag/method. Or at least have that as a future design goal.
iwoloschin commentedon Jan 15, 2020
Would it be possible, or sensical, to have a unique Exception for HTTP Status Code Errors? This is somewhat orthogonal to the specific request, and may make sense to be a separate issue. For example:
That'll work great and tell you it got a 404 error...unless it timed out.
That'll raise an
AttributeErrorbecause a timeout doesn't have a response, and aNoneTypecan't have a status_code! You can pretty easily inspect forresponse is not Noneor something like that, but it'd also be nice if you could do something like this:florimondmanca commentedon Jan 20, 2020
So, I think we're starting to settle down as a "no" on this? Or at least as something not crucial/possible to add incrementally anytime in the future, such as a flag to turn raise-on-error-response on… So, closing this for now…
As for differentiating between non-response errors, and response errors, we can still do…
emil45 commentedon Feb 2, 2021
I cannot understand why you backed off your "ready for production" pull request?
It feels like an intuitive approach using
AsyncClient(raise_for_status=True), and helps to clear a lot of DRY code.Even the competitors let the user choose https://docs.aiohttp.org/en/stable/client_reference.html ...
I don't think It will bother someone adding a
raise_for_statusat theFalsedefault state and let the user choose.If you don't have time, I can volunteer and do that, but I see the PR is already written and closed.
oprypin commentedon Mar 6, 2021
Seems like https://www.python-httpx.org/advanced/#event-hooks fully solves this issue, doesn't it?
florimondmanca commentedon Mar 6, 2021
@oprypin Yes. :)
Bobronium commentedon May 12, 2022
TL;DR:
For
httpx.Client:or
You don't have to declare
raise_on_4xx_5xx()like in docs example.For
httpx.AsyncClient(thx for mentioning this @facundoolano)
and then
or
facundoolano commentedon Oct 9, 2024
That doesn't seem to work for async requests, though:
zyv commentedon Feb 7, 2025
This is fantastic, thank you @Bobronium! But can this please be added to the documentation, e.g. here:
https://www.python-httpx.org/advanced/clients/
Would a PR be accepted?