Skip to content

Commit 3583fa7

Browse files
committed
Add Response.raise_for_excepted_status() method
Add a new method that raises HTTPStatusError unless the status code is explicitly listed in the expected parameter. Unlike `raise_for_status()`, this method requires all acceptable status codes (including 2xx) to be explicitly specified. Also refactors shared logic into `_ensure_request()` and `_raise_status_error()` helper methods.
1 parent ae1b9f6 commit 3583fa7

4 files changed

Lines changed: 135 additions & 11 deletions

File tree

docs/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
[total_seconds()](https://docs.python.org/3/library/datetime.html#datetime.timedelta.total_seconds) to correctly get
7272
the total elapsed seconds.
7373
* `def .raise_for_status()` - **Response**
74+
* `def .raise_for_excepted_status(expected)` - **Response**
7475
* `def .json()` - **Any**
7576
* `def .read()` - **bytes**
7677
* `def .iter_raw([chunk_size])` - **bytes iterator**

docs/quickstart.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,30 @@ The method returns the response instance, allowing you to use it inline. For exa
305305
>>> data = httpx.get('...').raise_for_status().json()
306306
```
307307

308+
### Allowing Specific Status Codes
309+
310+
Sometimes you may expect certain non-2xx status codes as valid responses (e.g., 404 when checking if a resource exists). Use `raise_for_excepted_status()` to specify which status codes are acceptable:
311+
312+
```pycon
313+
>>> r = httpx.get('https://httpbin.org/status/404')
314+
>>> r.raise_for_excepted_status([200, 404]) # 404 is expected, no exception raised
315+
<Response [404 Not Found]>
316+
```
317+
318+
Note that `raise_for_excepted_status()` only allows the status codes explicitly listed in the `expected` parameter. Even 2xx success codes must be included:
319+
320+
```pycon
321+
>>> r = httpx.get('https://httpbin.org/get')
322+
>>> r.status_code
323+
200
324+
>>> r.raise_for_excepted_status([201]) # 200 not in list, raises exception
325+
Traceback (most recent call last):
326+
...
327+
httpx._exceptions.HTTPStatusError: ...
328+
>>> r.raise_for_excepted_status([200, 201]) # 200 is in list, passes
329+
<Response [200 OK]>
330+
```
331+
308332
## Response Headers
309333

310334
The response headers are available as a dictionary-like interface.

httpx/_models.py

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -791,20 +791,19 @@ def has_redirect_location(self) -> bool:
791791
and "Location" in self.headers
792792
)
793793

794-
def raise_for_status(self) -> Response:
795-
"""
796-
Raise the `HTTPStatusError` if one occurred.
797-
"""
798-
request = self._request
799-
if request is None:
794+
def _ensure_request(self, method_name: str) -> Request:
795+
"""Ensure request is set, raise RuntimeError if not."""
796+
if self._request is None:
800797
raise RuntimeError(
801-
"Cannot call `raise_for_status` as the request "
798+
f"Cannot call `{method_name}` as the request "
802799
"instance has not been set on this response."
803800
)
801+
return self._request
804802

805-
if self.is_success:
806-
return self
807-
803+
def _raise_status_error(
804+
self, request: Request, *, error_type_for_2xx: str | None = None
805+
) -> typing.NoReturn:
806+
"""Internal helper to raise HTTPStatusError with appropriate message."""
808807
if self.has_redirect_location:
809808
message = (
810809
"{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
@@ -818,16 +817,58 @@ def raise_for_status(self) -> Response:
818817
)
819818

820819
status_class = self.status_code // 100
821-
error_types = {
820+
error_types: dict[int, str] = {
822821
1: "Informational response",
823822
3: "Redirect response",
824823
4: "Client error",
825824
5: "Server error",
826825
}
826+
if error_type_for_2xx is not None:
827+
error_types[2] = error_type_for_2xx
828+
827829
error_type = error_types.get(status_class, "Invalid status code")
828830
message = message.format(self, error_type=error_type)
829831
raise HTTPStatusError(message, request=request, response=self)
830832

833+
def raise_for_status(self) -> Response:
834+
"""
835+
Raise the `HTTPStatusError` if one occurred.
836+
"""
837+
request = self._ensure_request("raise_for_status")
838+
839+
if self.is_success:
840+
return self
841+
842+
self._raise_status_error(request)
843+
844+
def raise_for_excepted_status(
845+
self, expected: typing.Sequence[int]
846+
) -> Response:
847+
"""
848+
Raise the `HTTPStatusError` unless the status code is in the `expected` list.
849+
850+
Only status codes explicitly listed in `expected` are allowed to pass.
851+
All other status codes (including 2xx) will raise an exception.
852+
853+
Args:
854+
expected: A sequence of status codes that are considered acceptable
855+
and should not raise an exception.
856+
857+
Returns:
858+
This response instance if the status code is in the expected list.
859+
860+
Raises:
861+
HTTPStatusError: If the response status code is not in the expected list.
862+
"""
863+
request = self._ensure_request("raise_for_excepted_status")
864+
865+
if self.status_code in expected:
866+
return self
867+
868+
self._raise_status_error(
869+
request, error_type_for_2xx="Unexpected success response"
870+
)
871+
831872
def json(self, **kwargs: typing.Any) -> typing.Any:
832873
return jsonlib.loads(self.content, **kwargs)
833874

tests/models/test_responses.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,64 @@ def test_raise_for_status():
146146
response.raise_for_status()
147147

148148

149+
def test_raise_for_excepted_status():
150+
request = httpx.Request("GET", "https://example.org")
151+
152+
# 2xx status code in expected list - should pass
153+
response = httpx.Response(200, request=request)
154+
assert response.raise_for_excepted_status([200]) is response
155+
156+
# 2xx status code NOT in expected list - should raise with "Unexpected success"
157+
response = httpx.Response(200, request=request)
158+
with pytest.raises(httpx.HTTPStatusError) as exc_info:
159+
response.raise_for_excepted_status([201, 204])
160+
assert "Unexpected success response '200 OK'" in str(exc_info.value)
161+
162+
# 4xx status code in expected list - should pass
163+
response = httpx.Response(404, request=request)
164+
assert response.raise_for_excepted_status([200, 404]) is response
165+
166+
# 4xx status code NOT in expected list - should raise
167+
response = httpx.Response(404, request=request)
168+
with pytest.raises(httpx.HTTPStatusError) as exc_info:
169+
response.raise_for_excepted_status([200, 400])
170+
assert "Client error '404 Not Found'" in str(exc_info.value)
171+
172+
# 5xx status code in expected list - should pass
173+
response = httpx.Response(500, request=request)
174+
assert response.raise_for_excepted_status([500, 502, 503]) is response
175+
176+
# 5xx status code NOT in expected list - should raise
177+
response = httpx.Response(500, request=request)
178+
with pytest.raises(httpx.HTTPStatusError) as exc_info:
179+
response.raise_for_excepted_status([200])
180+
assert "Server error '500 Internal Server Error'" in str(exc_info.value)
181+
182+
# 3xx redirect in expected list - should pass
183+
headers = {"location": "https://other.org"}
184+
response = httpx.Response(301, headers=headers, request=request)
185+
assert response.raise_for_excepted_status([301, 302]) is response
186+
187+
# 3xx redirect NOT in expected list - should raise with redirect location
188+
response = httpx.Response(301, headers=headers, request=request)
189+
with pytest.raises(httpx.HTTPStatusError) as exc_info:
190+
response.raise_for_excepted_status([200])
191+
assert "Redirect response '301 Moved Permanently'" in str(exc_info.value)
192+
assert "Redirect location: 'https://other.org'" in str(exc_info.value)
193+
194+
# Empty expected list - all status codes should raise
195+
response = httpx.Response(200, request=request)
196+
with pytest.raises(httpx.HTTPStatusError):
197+
response.raise_for_excepted_status([])
198+
199+
# Calling .raise_for_excepted_status without setting a request instance
200+
# should raise a runtime error.
201+
response = httpx.Response(200)
202+
with pytest.raises(RuntimeError) as exc_info:
203+
response.raise_for_excepted_status([200])
204+
assert "raise_for_excepted_status" in str(exc_info.value)
205+
206+
149207
def test_response_repr():
150208
response = httpx.Response(
151209
200,

0 commit comments

Comments
 (0)