Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,32 @@ The HTTP `GET`, `DELETE`, `HEAD`, and `OPTIONS` methods are specified as not sup

If you really do need to send request data using these http methods you should use the generic `.request` function instead.

## Checking for 4xx/5xx responses
## Checking for 4xx/5xx error responses

Unlike Requests, HTTPX raises an `HTTPError` when a client error response (4xx) or a server error response (5xx) is received. This is done by automatically calling `response.raise_for_status()`.

```python
>>> import httpx
>>> httpx.get('https://httpbin.org/status/400')
Traceback (most recent call last):
[...]
httpx.exceptions.HTTPError: 400 Client Error: Bad Request [...]
```

You can disable this behavior by passing `raise_for_status=False` when issuing the request. You can then perform custom error handling, or raise an `HTTPError` by calling `.raise_for_status()` manually.

```python
>>> import httpx
>>> response = httpx.get('https://httpbin.org/status/400', raise_for_status=False)
>>> response
<Response [400 Bad Request]>
>>> response.is_error
True
>>> response.raise_for_status()
Traceback (most recent call last):
[...]
httpx.exceptions.HTTPError: 400 Client Error: Bad Request
```

We don't support `response.is_ok` since the naming is ambiguous there, and might incorrectly imply an equivalence to `response.status_code == codes.OK`. Instead we provide the `response.is_error` property. Use `if not response.is_error:` instead of `if response.is_ok:`.

Expand Down
20 changes: 20 additions & 0 deletions httpx/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def request(
auth: AuthTypes = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
allow_redirects: bool = True,
raise_for_status: bool = True,
verify: VerifyTypes = True,
cert: CertTypes = None,
trust_env: bool = True,
Expand Down Expand Up @@ -57,6 +58,8 @@ def request(
* **timeout** - *(optional)* The timeout configuration to use when sending
the request.
* **allow_redirects** - *(optional)* Enables or disables HTTP redirects.
* **raise_for_status** - *(optional)* Whether receiving an error response should
result in raising an `HTTPError`.
* **verify** - *(optional)* SSL certificates (a.k.a CA bundle) used to
verify the identity of requested hosts. Either `True` (default CA bundle),
a path to an SSL certificate file, or `False` (disable verification).
Expand Down Expand Up @@ -94,6 +97,7 @@ def request(
cookies=cookies,
auth=auth,
allow_redirects=allow_redirects,
raise_for_status=raise_for_status,
)


Expand All @@ -110,6 +114,7 @@ def stream(
auth: AuthTypes = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
allow_redirects: bool = True,
raise_for_status: bool = True,
verify: VerifyTypes = True,
cert: CertTypes = None,
trust_env: bool = True,
Expand All @@ -131,6 +136,7 @@ def stream(
auth=auth,
timeout=timeout,
allow_redirects=allow_redirects,
raise_for_status=raise_for_status,
close_client=True,
)

Expand All @@ -143,6 +149,7 @@ def get(
cookies: CookieTypes = None,
auth: AuthTypes = None,
allow_redirects: bool = True,
raise_for_status: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
Expand All @@ -164,6 +171,7 @@ def get(
cookies=cookies,
auth=auth,
allow_redirects=allow_redirects,
raise_for_status=raise_for_status,
cert=cert,
verify=verify,
timeout=timeout,
Expand All @@ -179,6 +187,7 @@ def options(
cookies: CookieTypes = None,
auth: AuthTypes = None,
allow_redirects: bool = True,
raise_for_status: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
Expand All @@ -200,6 +209,7 @@ def options(
cookies=cookies,
auth=auth,
allow_redirects=allow_redirects,
raise_for_status=raise_for_status,
cert=cert,
verify=verify,
timeout=timeout,
Expand All @@ -215,6 +225,7 @@ def head(
cookies: CookieTypes = None,
auth: AuthTypes = None,
allow_redirects: bool = False, # Note: Differs to usual default.
raise_for_status: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
Expand All @@ -238,6 +249,7 @@ def head(
cookies=cookies,
auth=auth,
allow_redirects=allow_redirects,
raise_for_status=raise_for_status,
cert=cert,
verify=verify,
timeout=timeout,
Expand All @@ -256,6 +268,7 @@ def post(
cookies: CookieTypes = None,
auth: AuthTypes = None,
allow_redirects: bool = True,
raise_for_status: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
Expand All @@ -277,6 +290,7 @@ def post(
cookies=cookies,
auth=auth,
allow_redirects=allow_redirects,
raise_for_status=raise_for_status,
cert=cert,
verify=verify,
timeout=timeout,
Expand All @@ -295,6 +309,7 @@ def put(
cookies: CookieTypes = None,
auth: AuthTypes = None,
allow_redirects: bool = True,
raise_for_status: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
Expand All @@ -316,6 +331,7 @@ def put(
cookies=cookies,
auth=auth,
allow_redirects=allow_redirects,
raise_for_status=raise_for_status,
cert=cert,
verify=verify,
timeout=timeout,
Expand All @@ -334,6 +350,7 @@ def patch(
cookies: CookieTypes = None,
auth: AuthTypes = None,
allow_redirects: bool = True,
raise_for_status: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
Expand All @@ -355,6 +372,7 @@ def patch(
cookies=cookies,
auth=auth,
allow_redirects=allow_redirects,
raise_for_status=raise_for_status,
cert=cert,
verify=verify,
timeout=timeout,
Expand All @@ -370,6 +388,7 @@ def delete(
cookies: CookieTypes = None,
auth: AuthTypes = None,
allow_redirects: bool = True,
raise_for_status: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
Expand All @@ -391,6 +410,7 @@ def delete(
cookies=cookies,
auth=auth,
allow_redirects=allow_redirects,
raise_for_status=raise_for_status,
cert=cert,
verify=verify,
timeout=timeout,
Expand Down
28 changes: 26 additions & 2 deletions httpx/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ def stream(
cookies: CookieTypes = None,
auth: AuthTypes = None,
allow_redirects: bool = True,
raise_for_status: bool = True,
timeout: typing.Union[TimeoutTypes, UnsetType] = UNSET,
) -> "StreamContextManager":
request = self.build_request(
Expand All @@ -179,6 +180,7 @@ def stream(
request=request,
auth=auth,
allow_redirects=allow_redirects,
raise_for_status=raise_for_status,
timeout=timeout,
)

Expand Down Expand Up @@ -552,6 +554,7 @@ def request(
cookies: CookieTypes = None,
auth: AuthTypes = None,
allow_redirects: bool = True,
raise_for_status: bool = True,
timeout: typing.Union[TimeoutTypes, UnsetType] = UNSET,
) -> Response:
request = self.build_request(
Expand All @@ -565,7 +568,11 @@ def request(
cookies=cookies,
)
return self.send(
request, auth=auth, allow_redirects=allow_redirects, timeout=timeout,
request,
auth=auth,
allow_redirects=allow_redirects,
raise_for_status=raise_for_status,
timeout=timeout,
)

def send(
Expand All @@ -575,6 +582,7 @@ def send(
stream: bool = False,
auth: AuthTypes = None,
allow_redirects: bool = True,
raise_for_status: bool = True,
timeout: typing.Union[TimeoutTypes, UnsetType] = UNSET,
) -> Response:
if request.url.scheme not in ("http", "https"):
Expand All @@ -588,6 +596,9 @@ def send(
request, auth=auth, timeout=timeout, allow_redirects=allow_redirects,
)

if raise_for_status:
response.raise_for_status()

if not stream:
try:
response.read()
Expand Down Expand Up @@ -1075,6 +1086,7 @@ async def request(
cookies: CookieTypes = None,
auth: AuthTypes = None,
allow_redirects: bool = True,
raise_for_status: bool = True,
timeout: typing.Union[TimeoutTypes, UnsetType] = UNSET,
) -> Response:
request = self.build_request(
Expand All @@ -1088,7 +1100,11 @@ async def request(
cookies=cookies,
)
response = await self.send(
request, auth=auth, allow_redirects=allow_redirects, timeout=timeout,
request,
auth=auth,
allow_redirects=allow_redirects,
raise_for_status=raise_for_status,
timeout=timeout,
)
return response

Expand All @@ -1099,6 +1115,7 @@ async def send(
stream: bool = False,
auth: AuthTypes = None,
allow_redirects: bool = True,
raise_for_status: bool = False,
timeout: typing.Union[TimeoutTypes, UnsetType] = UNSET,
) -> Response:
if request.url.scheme not in ("http", "https"):
Expand All @@ -1112,6 +1129,9 @@ async def send(
request, auth=auth, timeout=timeout, allow_redirects=allow_redirects,
)

if raise_for_status:
response.raise_for_status()

if not stream:
try:
await response.aread()
Expand Down Expand Up @@ -1410,13 +1430,15 @@ def __init__(
*,
auth: AuthTypes = None,
allow_redirects: bool = True,
raise_for_status: bool = True,
timeout: typing.Union[TimeoutTypes, UnsetType] = UNSET,
close_client: bool = False,
) -> None:
self.client = client
self.request = request
self.auth = auth
self.allow_redirects = allow_redirects
self.raise_for_status = raise_for_status
self.timeout = timeout
self.close_client = close_client

Expand All @@ -1426,6 +1448,7 @@ def __enter__(self) -> "Response":
request=self.request,
auth=self.auth,
allow_redirects=self.allow_redirects,
raise_for_status=self.raise_for_status,
timeout=self.timeout,
stream=True,
)
Expand All @@ -1448,6 +1471,7 @@ async def __aenter__(self) -> "Response":
request=self.request,
auth=self.auth,
allow_redirects=self.allow_redirects,
raise_for_status=self.raise_for_status,
timeout=self.timeout,
stream=True,
)
Expand Down
45 changes: 35 additions & 10 deletions tests/client/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest

import httpx
from tests.compat import nullcontext


@pytest.mark.usefixtures("async_environment")
Expand Down Expand Up @@ -83,19 +84,43 @@ async def hello_world():


@pytest.mark.usefixtures("async_environment")
async def test_raise_for_status(server):
@pytest.mark.parametrize(
"status_code, is_error_response",
[(200, False), (400, True), (404, True), (500, True), (505, True)],
)
async def test_raise_for_status_manual(server, status_code, is_error_response):
async with httpx.AsyncClient() as client:
for status_code in (200, 400, 404, 500, 505):
response = await client.request(
"GET", server.url.copy_with(path=f"/status/{status_code}")
response = await client.request(
"GET",
server.url.copy_with(path=f"/status/{status_code}"),
raise_for_status=False,
)

if is_error_response:
with pytest.raises(httpx.HTTPError) as exc_info:
response.raise_for_status()
assert exc_info.value.response == response
else:
assert response.raise_for_status() is None


@pytest.mark.usefixtures("async_environment")
@pytest.mark.parametrize(
"status_code, is_error_response",
[(200, False), (400, True), (404, True), (500, True), (505, True)],
)
async def test_raise_for_status_auto(server, status_code, is_error_response):
async with httpx.AsyncClient() as client:
with (
pytest.raises(httpx.HTTPError) if is_error_response else nullcontext()
) as exc_info:
await client.request(
"GET", server.url.copy_with(path=f"/status/{status_code}"),
)

if 400 <= status_code < 600:
with pytest.raises(httpx.HTTPError) as exc_info:
response.raise_for_status()
assert exc_info.value.response == response
else:
assert response.raise_for_status() is None
if is_error_response:
assert exc_info.value.response is not None
assert exc_info.value.response.status_code == status_code


@pytest.mark.usefixtures("async_environment")
Expand Down
Loading