Exception hierarchy

HTTPX · Practical · 9 min

Correct error handling separates 'the server said no' from 'we never got an answer', which need different reactions.

All HTTPX request errors derive from httpx.HTTPError, which has two main branches:

  • httpx.RequestError: something went wrong while sending or receiving, so there is no complete response. Subclasses include TransportError with TimeoutException (ConnectTimeout, ReadTimeout, WriteTimeout, PoolTimeout), NetworkError (ConnectError, ReadError, WriteError, CloseError), ProtocolError (malformed HTTP), ProxyError and UnsupportedProtocol (for example a missing https://). Also DecodingError and TooManyRedirects.
  • httpx.HTTPStatusError: raised only by raise_for_status() for 4xx and 5xx responses.

Every RequestError carries .request, so you can log which URL failed. HTTPStatusError carries both .request and .response.

A typical pattern:

try:
    r = client.get(url)
    r.raise_for_status()
except httpx.HTTPStatusError as e: ...  # server answered with an error
except httpx.RequestError as e: ...     # network-level failure

Some exceptions live outside this hierarchy because they are programming errors rather than request failures: httpx.InvalidURL, httpx.CookieConflict, and StreamError subclasses such as ResponseNotRead (accessing .content of a streamed response that was not read) or StreamConsumed.

Unlike Requests, HTTPX exceptions do not inherit from OSError, and the names are httpx-specific, so update except clauses when migrating.

Example

import httpx

def check(url):
    try:
        httpx.get(url, timeout=2.0).raise_for_status()
        return "ok"
    except httpx.HTTPStatusError as e:
        return f"status {e.response.status_code} from {e.request.url.path}"
    except httpx.TimeoutException as e:
        return f"timeout: {type(e).__name__}"
    except httpx.RequestError as e:
        return f"request failed: {type(e).__name__}"

print(check("https://httpbin.org/get"))         # ok
print(check("https://httpbin.org/status/500"))  # status 500 from /status/500
print(check("https://httpbin.org/delay/5"))     # timeout: ReadTimeout
print(check("ftp://example.com"))               # request failed: UnsupportedProtocol
print(issubclass(httpx.ConnectError, httpx.RequestError))  # True

Remember

  • RequestError: no complete response (timeouts, network, protocol)
  • HTTPStatusError: raised by raise_for_status for 4xx/5xx
  • Both inherit from httpx.HTTPError

Watch out for

  • Catching only HTTPStatusError and letting timeouts crash the program.

Check yourself

  1. Which exception means the server did answer, but with an error status?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.python-httpx.org/exceptions/