UnsetType,
)
from ._content_streams import ContentStream
-from ._exceptions import HTTPError, InvalidURL, RequestBodyUnavailable, TooManyRedirects
+from ._exceptions import (
+ HTTPCORE_EXC_MAP,
+ HTTPError,
+ InvalidURL,
+ RequestBodyUnavailable,
+ TooManyRedirects,
+ map_exceptions,
+)
from ._models import URL, Cookies, Headers, Origin, QueryParams, Request, Response
from ._status_codes import codes
from ._transports.asgi import ASGITransport
transport = self.transport_for_url(request.url)
try:
- (
- http_version,
- status_code,
- reason_phrase,
- headers,
- stream,
- ) = transport.request(
- request.method.encode(),
- request.url.raw,
- headers=request.headers.raw,
- stream=request.stream,
- timeout=timeout.as_dict(),
- )
+ with map_exceptions(HTTPCORE_EXC_MAP):
+ (
+ http_version,
+ status_code,
+ reason_phrase,
+ headers,
+ stream,
+ ) = transport.request(
+ request.method.encode(),
+ request.url.raw,
+ headers=request.headers.raw,
+ stream=request.stream,
+ timeout=timeout.as_dict(),
+ )
except HTTPError as exc:
# Add the original request to any HTTPError unless
# there'a already a request attached in the case of
transport = self.transport_for_url(request.url)
try:
- (
- http_version,
- status_code,
- reason_phrase,
- headers,
- stream,
- ) = await transport.request(
- request.method.encode(),
- request.url.raw,
- headers=request.headers.raw,
- stream=request.stream,
- timeout=timeout.as_dict(),
- )
+ with map_exceptions(HTTPCORE_EXC_MAP):
+ (
+ http_version,
+ status_code,
+ reason_phrase,
+ headers,
+ stream,
+ ) = await transport.request(
+ request.method.encode(),
+ request.url.raw,
+ headers=request.headers.raw,
+ stream=request.stream,
+ timeout=timeout.as_dict(),
+ )
except HTTPError as exc:
# Add the original request to any HTTPError unless
# there'a already a request attached in the case of
+import contextlib
import typing
import httpcore
# Timeout exceptions...
-ConnectTimeout = httpcore.ConnectTimeout
-ReadTimeout = httpcore.ReadTimeout
-WriteTimeout = httpcore.WriteTimeout
-PoolTimeout = httpcore.PoolTimeout
+
+class TimeoutException(HTTPError):
+ """
+ The base class for timeout errors.
+
+ An operation has timed out.
+ """
+
+
+class ConnectTimeout(TimeoutException):
+ """
+ Timed out while connecting to the host.
+ """
+
+
+class ReadTimeout(TimeoutException):
+ """
+ Timed out while receiving data from the host.
+ """
+
+
+class WriteTimeout(TimeoutException):
+ """
+ Timed out while sending data to the host.
+ """
+
+
+class PoolTimeout(TimeoutException):
+ """
+ Timed out waiting to acquire a connection from the pool.
+ """
# Core networking exceptions...
-NetworkError = httpcore.NetworkError
-ReadError = httpcore.ReadError
-WriteError = httpcore.WriteError
-ConnectError = httpcore.ConnectError
-CloseError = httpcore.CloseError
+
+class NetworkError(HTTPError):
+ """
+ The base class for network-related errors.
+
+ An error occurred while interacting with the network.
+ """
+
+
+class ReadError(NetworkError):
+ """
+ Failed to receive data from the network.
+ """
+
+
+class WriteError(NetworkError):
+ """
+ Failed to send data through the network.
+ """
+
+
+class ConnectError(NetworkError):
+ """
+ Failed to establish a connection.
+ """
+
+
+class CloseError(NetworkError):
+ """
+ Failed to close a connection.
+ """
# Other transport exceptions...
-ProxyError = httpcore.ProxyError
-ProtocolError = httpcore.ProtocolError
+
+class ProxyError(HTTPError):
+ """
+ An error occurred while proxying a request.
+ """
+
+
+class ProtocolError(HTTPError):
+ """
+ A protocol was violated by the server.
+ """
# HTTP exceptions...
"""
Attempted to lookup a cookie by name, but multiple cookies existed.
"""
+
+
+@contextlib.contextmanager
+def map_exceptions(
+ mapping: typing.Mapping[typing.Type[Exception], typing.Type[Exception]]
+) -> typing.Iterator[None]:
+ try:
+ yield
+ except Exception as exc:
+ mapped_exc = None
+
+ for from_exc, to_exc in mapping.items():
+ if not isinstance(exc, from_exc):
+ continue
+ # We want to map to the most specific exception we can find.
+ # Eg if `exc` is an `httpcore.ReadTimeout`, we want to map to
+ # `httpx.ReadTimeout`, not just `httpx.TimeoutException`.
+ if mapped_exc is None or issubclass(to_exc, mapped_exc):
+ mapped_exc = to_exc
+
+ if mapped_exc is None:
+ raise
+
+ raise mapped_exc(exc) from None
+
+
+HTTPCORE_EXC_MAP = {
+ httpcore.TimeoutException: TimeoutException,
+ httpcore.ConnectTimeout: ConnectTimeout,
+ httpcore.ReadTimeout: ReadTimeout,
+ httpcore.WriteTimeout: WriteTimeout,
+ httpcore.PoolTimeout: PoolTimeout,
+ httpcore.NetworkError: NetworkError,
+ httpcore.ConnectError: ConnectError,
+ httpcore.ReadError: ReadError,
+ httpcore.WriteError: WriteError,
+ httpcore.CloseError: CloseError,
+ httpcore.ProxyError: ProxyError,
+ httpcore.ProtocolError: ProtocolError,
+}
+from typing import Any
+
+import httpcore
import pytest
import httpx
+from httpx._exceptions import HTTPCORE_EXC_MAP
+
+
+def test_httpcore_all_exceptions_mapped() -> None:
+ """
+ All exception classes exposed by HTTPCore are properly mapped to an HTTPX-specific
+ exception class.
+ """
+ not_mapped = [
+ value
+ for name, value in vars(httpcore).items()
+ if isinstance(value, type)
+ and issubclass(value, Exception)
+ and value not in HTTPCORE_EXC_MAP
+ ]
+
+ if not_mapped:
+ pytest.fail(f"Unmapped httpcore exceptions: {not_mapped}")
+
+
+def test_httpcore_exception_mapping() -> None:
+ """
+ HTTPCore exception mapping works as expected.
+ """
+
+ # Make sure we don't just map to `NetworkError`.
+ with pytest.raises(httpx.ConnectError):
+ httpx.get("http://doesnotexist")
+
+ # Make sure it also works with custom transports.
+ class MockTransport(httpcore.SyncHTTPTransport):
+ def request(self, *args: Any, **kwargs: Any) -> Any:
+ raise httpcore.ProtocolError()
+
+ client = httpx.Client(transport=MockTransport())
+ with pytest.raises(httpx.ProtocolError):
+ client.get("http://testserver")
def test_httpx_exceptions_exposed() -> None: