From d449fe9e88a31598be70ed58634dd3a0a41d8a87 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 24 Feb 2026 16:53:52 +0200 Subject: [PATCH 1/2] Refactor `map_httpcore_exceptions` to not be a context manager `contextlib.contextmanager`s are much slower than `try:except:`, and here they occur in very hot paths. --- src/httpx2/httpx2/_transports/default.py | 72 ++++++++++++++---------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/src/httpx2/httpx2/_transports/default.py b/src/httpx2/httpx2/_transports/default.py index 17d48c4f..d45b7f95 100644 --- a/src/httpx2/httpx2/_transports/default.py +++ b/src/httpx2/httpx2/_transports/default.py @@ -26,9 +26,8 @@ from __future__ import annotations -import contextlib import typing -from collections.abc import Generator +from functools import cache from types import TracebackType if typing.TYPE_CHECKING: # pragma: no cover @@ -65,9 +64,8 @@ __all__ = ["AsyncHTTPTransport", "HTTPTransport"] -HTTPCORE_EXC_MAP: dict[type[Exception], type[httpx2.HTTPError]] = {} - +@cache def _load_httpcore_exceptions() -> dict[type[Exception], type[httpx2.HTTPError]]: import httpcore2 @@ -89,30 +87,30 @@ def _load_httpcore_exceptions() -> dict[type[Exception], type[httpx2.HTTPError]] } -@contextlib.contextmanager -def map_httpcore_exceptions() -> Generator[None]: - global HTTPCORE_EXC_MAP - if len(HTTPCORE_EXC_MAP) == 0: - HTTPCORE_EXC_MAP = _load_httpcore_exceptions() - try: - yield - except Exception as exc: - mapped_exc = None +@cache +def _get_httpcore_exception_types() -> tuple[type[Exception], ...]: + return tuple(_load_httpcore_exceptions()) + - for from_exc, to_exc in HTTPCORE_EXC_MAP.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 `httpcore2.ReadTimeout`, we want to map to - # `httpx2.ReadTimeout`, not just `httpx2.TimeoutException`. - if mapped_exc is None or issubclass(to_exc, mapped_exc): - mapped_exc = to_exc +def _map_httpcore_exception(exc: Exception) -> httpx2.HTTPError: + """ + Map the given httpcore exception to the corresponding HTTPX exception, + and return it. If there is no equivalence, raise immediately. + """ + mapped_exc = None + for from_exc, to_exc in _load_httpcore_exceptions().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 `httpcore2.ReadTimeout`, we want to map to + # `httpx2.ReadTimeout`, not just `httpx2.TimeoutException`. + if mapped_exc is None or issubclass(to_exc, mapped_exc): + mapped_exc = to_exc - if mapped_exc is None: # pragma: no cover - raise + if mapped_exc is None: # pragma: no cover + raise - message = str(exc) - raise mapped_exc(message) from exc + return mapped_exc(str(exc)) class ResponseStream(SyncByteStream): @@ -120,8 +118,10 @@ def __init__(self, httpcore_stream: typing.Iterable[bytes]) -> None: self._httpcore_stream = httpcore_stream def __iter__(self) -> typing.Iterator[bytes]: - with map_httpcore_exceptions(): + try: yield from self._httpcore_stream + except _get_httpcore_exception_types() as exc: + raise _map_httpcore_exception(exc) from exc def close(self) -> None: if hasattr(self._httpcore_stream, "close"): @@ -219,8 +219,10 @@ def __exit__( exc_value: BaseException | None = None, traceback: TracebackType | None = None, ) -> None: - with map_httpcore_exceptions(): + try: self._pool.__exit__(exc_type, exc_value, traceback) + except _get_httpcore_exception_types() as exc: # pragma: no cover + raise _map_httpcore_exception(exc) from exc def handle_request( self, @@ -241,8 +243,10 @@ def handle_request( content=request.stream, extensions=request.extensions, ) - with map_httpcore_exceptions(): + try: resp = self._pool.handle_request(req) + except _get_httpcore_exception_types() as exc: + raise _map_httpcore_exception(exc) from exc assert isinstance(resp.stream, typing.Iterable) @@ -262,9 +266,11 @@ def __init__(self, httpcore_stream: typing.AsyncIterable[bytes]) -> None: self._httpcore_stream = httpcore_stream async def __aiter__(self) -> typing.AsyncIterator[bytes]: - with map_httpcore_exceptions(): + try: async for part in self._httpcore_stream: yield part + except _get_httpcore_exception_types() as exc: # pragma: no cover + raise _map_httpcore_exception(exc) from exc async def aclose(self) -> None: if hasattr(self._httpcore_stream, "aclose"): @@ -362,8 +368,10 @@ async def __aexit__( exc_value: BaseException | None = None, traceback: TracebackType | None = None, ) -> None: - with map_httpcore_exceptions(): + try: await self._pool.__aexit__(exc_type, exc_value, traceback) + except _get_httpcore_exception_types() as exc: # pragma: no cover + raise _map_httpcore_exception(exc) from exc async def handle_async_request( self, @@ -384,8 +392,10 @@ async def handle_async_request( content=request.stream, extensions=request.extensions, ) - with map_httpcore_exceptions(): + try: resp = await self._pool.handle_async_request(req) + except _get_httpcore_exception_types() as exc: + raise _map_httpcore_exception(exc) from exc assert isinstance(resp.stream, typing.AsyncIterable) From 384ae314a777480c6c819cb938a2171fcf252f12 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 16 Jun 2026 13:32:41 +0300 Subject: [PATCH 2/2] Retire map_exceptions in httpcore2 `try: except:` blocks are free on modern Pythons when exceptions aren't raised. Entering a `@contextmanager`'d block is much less free, and there are hot paths (e.g. reading from a sync socket) where it's worth avoiding that overhead. For consistency, this retires the use of `map_exceptions` everywhere. New exception blocks are marked nocover, since they weren't actually tested before either. --- src/httpcore2/httpcore2/_async/http11.py | 9 ++- src/httpcore2/httpcore2/_backends/anyio.py | 69 +++++++++++---------- src/httpcore2/httpcore2/_backends/sync.py | 60 ++++++++++-------- src/httpcore2/httpcore2/_backends/trio.py | 62 +++++++++--------- src/httpcore2/httpcore2/_exceptions.py | 17 ----- src/httpcore2/httpcore2/_sync/http11.py | 9 ++- src/httpcore2/httpcore2/_synchronization.py | 12 ++-- 7 files changed, 121 insertions(+), 117 deletions(-) diff --git a/src/httpcore2/httpcore2/_async/http11.py b/src/httpcore2/httpcore2/_async/http11.py index 769825d9..76d92dbc 100644 --- a/src/httpcore2/httpcore2/_async/http11.py +++ b/src/httpcore2/httpcore2/_async/http11.py @@ -16,7 +16,6 @@ LocalProtocolError, RemoteProtocolError, WriteError, - map_exceptions, ) from .._models import Origin, Request, Response from .._synchronization import AsyncLock, AsyncShieldCancellation @@ -130,12 +129,14 @@ async def _send_request_headers(self, request: Request) -> None: timeouts = request.extensions.get("timeout", {}) timeout = timeouts.get("write", None) - with map_exceptions({h11.LocalProtocolError: LocalProtocolError}): + try: event = h11.Request( method=request.method, target=request.url.target, headers=request.headers, ) + except h11.LocalProtocolError as exc: + raise LocalProtocolError(exc) from exc await self._send_event(event, timeout=timeout) async def _send_request_body(self, request: Request) -> None: @@ -193,8 +194,10 @@ async def _receive_response_body(self, request: Request) -> AsyncGenerator[bytes async def _receive_event(self, timeout: float | None = None) -> h11.Event | type[h11.PAUSED]: while True: - with map_exceptions({h11.RemoteProtocolError: RemoteProtocolError}): + try: event = self._h11_state.next_event() + except h11.RemoteProtocolError as exc: + raise RemoteProtocolError(exc) from exc if event is h11.NEED_DATA: data = await self._network_stream.read(self.READ_NUM_BYTES, timeout=timeout) diff --git a/src/httpcore2/httpcore2/_backends/anyio.py b/src/httpcore2/httpcore2/_backends/anyio.py index 16c270e8..b7c2961b 100644 --- a/src/httpcore2/httpcore2/_backends/anyio.py +++ b/src/httpcore2/httpcore2/_backends/anyio.py @@ -14,7 +14,6 @@ ReadTimeout, WriteError, WriteTimeout, - map_exceptions, ) from .._utils import is_socket_readable from .base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream @@ -25,31 +24,35 @@ def __init__(self, stream: anyio.abc.ByteStream) -> None: self._stream = stream async def read(self, max_bytes: int, timeout: float | None = None) -> bytes: - exc_map: dict[type[Exception], type[Exception]] = { - TimeoutError: ReadTimeout, - anyio.BrokenResourceError: ReadError, - anyio.ClosedResourceError: ReadError, - anyio.EndOfStream: ReadError, - } - with map_exceptions(exc_map): + try: with anyio.fail_after(timeout): try: return await self._stream.receive(max_bytes=max_bytes) except anyio.EndOfStream: # pragma: no cover return b"" + except TimeoutError as exc: # pragma: no cover + raise ReadTimeout(exc) from exc + except ( + anyio.BrokenResourceError, + anyio.ClosedResourceError, + anyio.EndOfStream, + ) as exc: # pragma: no cover + raise ReadError(exc) from exc async def write(self, buffer: bytes, timeout: float | None = None) -> None: if not buffer: return - exc_map: dict[type[Exception], type[Exception]] = { - TimeoutError: WriteTimeout, - anyio.BrokenResourceError: WriteError, - anyio.ClosedResourceError: WriteError, - } - with map_exceptions(exc_map): + try: with anyio.fail_after(timeout): await self._stream.send(item=buffer) + except TimeoutError as exc: # pragma: no cover + raise WriteTimeout(exc) from exc + except ( + anyio.BrokenResourceError, + anyio.ClosedResourceError, + ) as exc: # pragma: no cover + raise WriteError(exc) from exc async def aclose(self) -> None: await self._stream.aclose() @@ -60,13 +63,7 @@ async def start_tls( server_hostname: str | None = None, timeout: float | None = None, ) -> AsyncNetworkStream: - exc_map: dict[type[Exception], type[Exception]] = { - TimeoutError: ConnectTimeout, - anyio.BrokenResourceError: ConnectError, - anyio.EndOfStream: ConnectError, - ssl.SSLError: ConnectError, - } - with map_exceptions(exc_map): + try: try: with anyio.fail_after(timeout): ssl_stream = await anyio.streams.tls.TLSStream.wrap( @@ -79,6 +76,14 @@ async def start_tls( except Exception as exc: # pragma: no cover await self.aclose() raise exc + except TimeoutError as exc: # pragma: no cover + raise ConnectTimeout(exc) from exc + except ( + anyio.BrokenResourceError, + anyio.EndOfStream, + ssl.SSLError, + ) as exc: # pragma: no cover + raise ConnectError(exc) from exc return AnyIOStream(ssl_stream) def get_extra_info(self, info: str) -> typing.Any: @@ -107,12 +112,7 @@ async def connect_tcp( ) -> AsyncNetworkStream: # pragma: no cover if socket_options is None: socket_options = [] - exc_map: dict[type[Exception], type[Exception]] = { - TimeoutError: ConnectTimeout, - OSError: ConnectError, - anyio.BrokenResourceError: ConnectError, - } - with map_exceptions(exc_map): + try: with anyio.fail_after(timeout): stream: anyio.abc.ByteStream = await anyio.connect_tcp( remote_host=host, @@ -122,6 +122,10 @@ async def connect_tcp( # By default TCP sockets opened in `asyncio` include TCP_NODELAY. for option in socket_options: stream._raw_socket.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover + except TimeoutError as exc: + raise ConnectTimeout(exc) from exc + except (OSError, anyio.BrokenResourceError) as exc: + raise ConnectError(exc) from exc return AnyIOStream(stream) async def connect_unix_socket( @@ -132,16 +136,15 @@ async def connect_unix_socket( ) -> AsyncNetworkStream: # pragma: no cover if socket_options is None: socket_options = [] - exc_map: dict[type[Exception], type[Exception]] = { - TimeoutError: ConnectTimeout, - OSError: ConnectError, - anyio.BrokenResourceError: ConnectError, - } - with map_exceptions(exc_map): + try: with anyio.fail_after(timeout): stream: anyio.abc.ByteStream = await anyio.connect_unix(path) for option in socket_options: stream._raw_socket.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover + except TimeoutError as exc: + raise ConnectTimeout(exc) from exc + except (OSError, anyio.BrokenResourceError) as exc: + raise ConnectError(exc) from exc return AnyIOStream(stream) async def sleep(self, seconds: float) -> None: diff --git a/src/httpcore2/httpcore2/_backends/sync.py b/src/httpcore2/httpcore2/_backends/sync.py index 54ce5428..89f36ced 100644 --- a/src/httpcore2/httpcore2/_backends/sync.py +++ b/src/httpcore2/httpcore2/_backends/sync.py @@ -9,12 +9,10 @@ from .._exceptions import ( ConnectError, ConnectTimeout, - ExceptionMapping, ReadError, ReadTimeout, WriteError, WriteTimeout, - map_exceptions, ) from .._utils import is_socket_readable from .base import SOCKET_OPTION, NetworkBackend, NetworkStream @@ -77,19 +75,25 @@ def _perform_io( return ret def read(self, max_bytes: int, timeout: float | None = None) -> bytes: - exc_map: ExceptionMapping = {socket.timeout: ReadTimeout, OSError: ReadError} - with map_exceptions(exc_map): + try: self._sock.settimeout(timeout) return typing.cast(bytes, self._perform_io(functools.partial(self.ssl_obj.read, max_bytes))) + except TimeoutError as exc: + raise ReadTimeout(exc) from exc + except OSError as exc: + raise ReadError(exc) from exc def write(self, buffer: bytes, timeout: float | None = None) -> None: - exc_map: ExceptionMapping = {socket.timeout: WriteTimeout, OSError: WriteError} - with map_exceptions(exc_map): + try: self._sock.settimeout(timeout) view = memoryview(buffer) # zero-copy slicing; avoids copies while view: nsent = self._perform_io(functools.partial(self.ssl_obj.write, view)) view = view[nsent:] + except TimeoutError as exc: + raise WriteTimeout(exc) from exc + except OSError as exc: + raise WriteError(exc) from exc def close(self) -> None: self._sock.close() @@ -121,22 +125,28 @@ def __init__(self, sock: socket.socket) -> None: self._sock = sock def read(self, max_bytes: int, timeout: float | None = None) -> bytes: - exc_map: ExceptionMapping = {socket.timeout: ReadTimeout, OSError: ReadError} - with map_exceptions(exc_map): + try: self._sock.settimeout(timeout) return self._sock.recv(max_bytes) + except TimeoutError as exc: # pragma: no cover + raise ReadTimeout(exc) from exc + except OSError as exc: # pragma: no cover + raise ReadError(exc) from exc def write(self, buffer: bytes, timeout: float | None = None) -> None: if not buffer: return - exc_map: ExceptionMapping = {socket.timeout: WriteTimeout, OSError: WriteError} - with map_exceptions(exc_map): + try: view = memoryview(buffer) # zero-copy slicing; avoids copies while view: self._sock.settimeout(timeout) n = self._sock.send(view) view = view[n:] + except TimeoutError as exc: # pragma: no cover + raise WriteTimeout(exc) from exc + except OSError as exc: # pragma: no cover + raise WriteError(exc) from exc def close(self) -> None: self._sock.close() @@ -147,11 +157,7 @@ def start_tls( server_hostname: str | None = None, timeout: float | None = None, ) -> NetworkStream: - exc_map: ExceptionMapping = { - socket.timeout: ConnectTimeout, - OSError: ConnectError, - } - with map_exceptions(exc_map): + try: try: if isinstance(self._sock, ssl.SSLSocket): # pragma: no cover # If the underlying socket has already been upgraded @@ -164,6 +170,10 @@ def start_tls( except Exception as exc: # pragma: no cover self.close() raise exc + except TimeoutError as exc: # pragma: no cover + raise ConnectTimeout(exc) from exc + except OSError as exc: # pragma: no cover + raise ConnectError(exc) from exc return SyncStream(sock) def get_extra_info(self, info: str) -> typing.Any: @@ -195,12 +205,8 @@ def connect_tcp( socket_options = [] # pragma: no cover address = (host, port) source_address = None if local_address is None else (local_address, 0) - exc_map: ExceptionMapping = { - socket.timeout: ConnectTimeout, - OSError: ConnectError, - } - with map_exceptions(exc_map): + try: sock = socket.create_connection( address, timeout, @@ -209,6 +215,10 @@ def connect_tcp( for option in socket_options: sock.setsockopt(*option) # pragma: no cover sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + except TimeoutError as exc: # pragma: no cover + raise ConnectTimeout(exc) from exc + except OSError as exc: # pragma: no cover + raise ConnectError(exc) from exc return SyncStream(sock) def connect_unix_socket( @@ -222,14 +232,14 @@ def connect_unix_socket( if socket_options is None: socket_options = [] - exc_map: ExceptionMapping = { - socket.timeout: ConnectTimeout, - OSError: ConnectError, - } - with map_exceptions(exc_map): + try: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) for option in socket_options: sock.setsockopt(*option) sock.settimeout(timeout) sock.connect(path) + except TimeoutError as exc: + raise ConnectTimeout(exc) from exc + except OSError as exc: + raise ConnectError(exc) from exc return SyncStream(sock) diff --git a/src/httpcore2/httpcore2/_backends/trio.py b/src/httpcore2/httpcore2/_backends/trio.py index 742985b9..57a9b630 100644 --- a/src/httpcore2/httpcore2/_backends/trio.py +++ b/src/httpcore2/httpcore2/_backends/trio.py @@ -8,12 +8,10 @@ from .._exceptions import ( ConnectError, ConnectTimeout, - ExceptionMapping, ReadError, ReadTimeout, WriteError, WriteTimeout, - map_exceptions, ) from .base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream @@ -24,29 +22,33 @@ def __init__(self, stream: trio.abc.Stream) -> None: async def read(self, max_bytes: int, timeout: float | None = None) -> bytes: timeout_or_inf = float("inf") if timeout is None else timeout - exc_map: ExceptionMapping = { - trio.TooSlowError: ReadTimeout, - trio.BrokenResourceError: ReadError, - trio.ClosedResourceError: ReadError, - } - with map_exceptions(exc_map): + try: with trio.fail_after(timeout_or_inf): data: bytes = await self._stream.receive_some(max_bytes=max_bytes) return data + except trio.TooSlowError as exc: # pragma: no cover + raise ReadTimeout(exc) from exc + except ( + trio.BrokenResourceError, + trio.ClosedResourceError, + ) as exc: # pragma: no cover + raise ReadError(exc) from exc async def write(self, buffer: bytes, timeout: float | None = None) -> None: if not buffer: return timeout_or_inf = float("inf") if timeout is None else timeout - exc_map: ExceptionMapping = { - trio.TooSlowError: WriteTimeout, - trio.BrokenResourceError: WriteError, - trio.ClosedResourceError: WriteError, - } - with map_exceptions(exc_map): + try: with trio.fail_after(timeout_or_inf): await self._stream.send_all(data=buffer) + except trio.TooSlowError as exc: # pragma: no cover + raise WriteTimeout(exc) from exc + except ( + trio.BrokenResourceError, + trio.ClosedResourceError, + ) as exc: # pragma: no cover + raise WriteError(exc) from exc async def aclose(self) -> None: await self._stream.aclose() @@ -58,10 +60,6 @@ async def start_tls( timeout: float | None = None, ) -> AsyncNetworkStream: timeout_or_inf = float("inf") if timeout is None else timeout - exc_map: ExceptionMapping = { - trio.TooSlowError: ConnectTimeout, - trio.BrokenResourceError: ConnectError, - } ssl_stream = trio.SSLStream( self._stream, ssl_context=ssl_context, @@ -69,13 +67,17 @@ async def start_tls( https_compatible=True, server_side=False, ) - with map_exceptions(exc_map): + try: try: with trio.fail_after(timeout_or_inf): await ssl_stream.do_handshake() except Exception as exc: # pragma: no cover await self.aclose() raise exc + except trio.TooSlowError as exc: # pragma: no cover + raise ConnectTimeout(exc) from exc + except trio.BrokenResourceError as exc: # pragma: no cover + raise ConnectError(exc) from exc return TrioStream(ssl_stream) def get_extra_info(self, info: str) -> typing.Any: @@ -120,16 +122,15 @@ async def connect_tcp( if socket_options is None: socket_options = [] # pragma: no cover timeout_or_inf = float("inf") if timeout is None else timeout - exc_map: ExceptionMapping = { - trio.TooSlowError: ConnectTimeout, - trio.BrokenResourceError: ConnectError, - OSError: ConnectError, - } - with map_exceptions(exc_map): + try: with trio.fail_after(timeout_or_inf): stream: trio.abc.Stream = await trio.open_tcp_stream(host=host, port=port, local_address=local_address) for option in socket_options: stream.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover + except trio.TooSlowError as exc: # pragma: no cover + raise ConnectTimeout(exc) from exc + except (trio.BrokenResourceError, OSError) as exc: # pragma: no cover + raise ConnectError(exc) from exc return TrioStream(stream) async def connect_unix_socket( @@ -141,16 +142,15 @@ async def connect_unix_socket( if socket_options is None: socket_options = [] timeout_or_inf = float("inf") if timeout is None else timeout - exc_map: ExceptionMapping = { - trio.TooSlowError: ConnectTimeout, - trio.BrokenResourceError: ConnectError, - OSError: ConnectError, - } - with map_exceptions(exc_map): + try: with trio.fail_after(timeout_or_inf): stream: trio.abc.Stream = await trio.open_unix_socket(path) for option in socket_options: stream.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover + except trio.TooSlowError as exc: + raise ConnectTimeout(exc) from exc + except (trio.BrokenResourceError, OSError) as exc: + raise ConnectError(exc) from exc return TrioStream(stream) async def sleep(self, seconds: float) -> None: diff --git a/src/httpcore2/httpcore2/_exceptions.py b/src/httpcore2/httpcore2/_exceptions.py index a54d43b7..fbbbe550 100644 --- a/src/httpcore2/httpcore2/_exceptions.py +++ b/src/httpcore2/httpcore2/_exceptions.py @@ -1,22 +1,5 @@ from __future__ import annotations -import contextlib -import typing -from collections.abc import Generator - -ExceptionMapping = typing.Mapping[type[Exception], type[Exception]] - - -@contextlib.contextmanager -def map_exceptions(map: ExceptionMapping) -> Generator[None]: - try: - yield - except Exception as exc: # noqa: PIE786 - for from_exc, to_exc in map.items(): - if isinstance(exc, from_exc): - raise to_exc(exc) from exc - raise # pragma: no cover - class ConnectionNotAvailable(Exception): pass diff --git a/src/httpcore2/httpcore2/_sync/http11.py b/src/httpcore2/httpcore2/_sync/http11.py index 14d674ae..793a4c7a 100644 --- a/src/httpcore2/httpcore2/_sync/http11.py +++ b/src/httpcore2/httpcore2/_sync/http11.py @@ -16,7 +16,6 @@ LocalProtocolError, RemoteProtocolError, WriteError, - map_exceptions, ) from .._models import Origin, Request, Response from .._synchronization import Lock, ShieldCancellation @@ -130,12 +129,14 @@ def _send_request_headers(self, request: Request) -> None: timeouts = request.extensions.get("timeout", {}) timeout = timeouts.get("write", None) - with map_exceptions({h11.LocalProtocolError: LocalProtocolError}): + try: event = h11.Request( method=request.method, target=request.url.target, headers=request.headers, ) + except h11.LocalProtocolError as exc: + raise LocalProtocolError(exc) from exc self._send_event(event, timeout=timeout) def _send_request_body(self, request: Request) -> None: @@ -193,8 +194,10 @@ def _receive_response_body(self, request: Request) -> Generator[bytes]: def _receive_event(self, timeout: float | None = None) -> h11.Event | type[h11.PAUSED]: while True: - with map_exceptions({h11.RemoteProtocolError: RemoteProtocolError}): + try: event = self._h11_state.next_event() + except h11.RemoteProtocolError as exc: + raise RemoteProtocolError(exc) from exc if event is h11.NEED_DATA: data = self._network_stream.read(self.READ_NUM_BYTES, timeout=timeout) diff --git a/src/httpcore2/httpcore2/_synchronization.py b/src/httpcore2/httpcore2/_synchronization.py index 22287fc0..f75f1610 100644 --- a/src/httpcore2/httpcore2/_synchronization.py +++ b/src/httpcore2/httpcore2/_synchronization.py @@ -3,7 +3,7 @@ import threading import types -from ._exceptions import ExceptionMapping, PoolTimeout, map_exceptions +from ._exceptions import PoolTimeout # Our async synchronization primitives use either 'anyio' or 'trio' depending # on if they're running under asyncio or trio. @@ -135,16 +135,18 @@ async def wait(self, timeout: float | None = None) -> None: self.setup() if self._backend == "trio": - trio_exc_map: ExceptionMapping = {trio.TooSlowError: PoolTimeout} timeout_or_inf = float("inf") if timeout is None else timeout - with map_exceptions(trio_exc_map): + try: with trio.fail_after(timeout_or_inf): await self._trio_event.wait() + except trio.TooSlowError as exc: + raise PoolTimeout(exc) from exc elif self._backend == "asyncio": - anyio_exc_map: ExceptionMapping = {TimeoutError: PoolTimeout} - with map_exceptions(anyio_exc_map): + try: with anyio.fail_after(timeout): await self._anyio_event.wait() + except TimeoutError as exc: + raise PoolTimeout(exc) from exc class AsyncSemaphore: