Skip to content

Commit 09fc81a

Browse files
committed
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. Sibling of encode/httpcore#1044
1 parent b5addb6 commit 09fc81a

File tree

1 file changed

+37
-30
lines changed

1 file changed

+37
-30
lines changed

httpx/_transports/default.py

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626

2727
from __future__ import annotations
2828

29-
import contextlib
3029
import typing
30+
from functools import cache
3131
from types import TracebackType
3232

3333
if typing.TYPE_CHECKING:
@@ -68,9 +68,8 @@
6868

6969
__all__ = ["AsyncHTTPTransport", "HTTPTransport"]
7070

71-
HTTPCORE_EXC_MAP: dict[type[Exception], type[httpx.HTTPError]] = {}
72-
7371

72+
@cache
7473
def _load_httpcore_exceptions() -> dict[type[Exception], type[httpx.HTTPError]]:
7574
import httpcore
7675

@@ -92,40 +91,38 @@ def _load_httpcore_exceptions() -> dict[type[Exception], type[httpx.HTTPError]]:
9291
}
9392

9493

95-
@contextlib.contextmanager
96-
def map_httpcore_exceptions() -> typing.Iterator[None]:
97-
global HTTPCORE_EXC_MAP
98-
if len(HTTPCORE_EXC_MAP) == 0:
99-
HTTPCORE_EXC_MAP = _load_httpcore_exceptions()
100-
try:
101-
yield
102-
except Exception as exc:
103-
mapped_exc = None
94+
@cache
95+
def _get_httpcore_exception_types() -> tuple[type[Exception], ...]:
96+
return tuple(_load_httpcore_exceptions())
97+
10498

105-
for from_exc, to_exc in HTTPCORE_EXC_MAP.items():
106-
if not isinstance(exc, from_exc):
107-
continue
108-
# We want to map to the most specific exception we can find.
109-
# Eg if `exc` is an `httpcore.ReadTimeout`, we want to map to
110-
# `httpx.ReadTimeout`, not just `httpx.TimeoutException`.
111-
if mapped_exc is None or issubclass(to_exc, mapped_exc):
112-
mapped_exc = to_exc
99+
def _map_httpcore_exception(exc: Exception) -> httpx.HTTPError:
100+
mapped_exc = None
101+
for from_exc, to_exc in _load_httpcore_exceptions().items():
102+
if not isinstance(exc, from_exc):
103+
continue
104+
# We want to map to the most specific exception we can find.
105+
# Eg if `exc` is an `httpcore.ReadTimeout`, we want to map to
106+
# `httpx.ReadTimeout`, not just `httpx.TimeoutException`.
107+
if mapped_exc is None or issubclass(to_exc, mapped_exc):
108+
mapped_exc = to_exc
113109

114-
if mapped_exc is None: # pragma: no cover
115-
raise
110+
if mapped_exc is None: # pragma: no cover
111+
raise
116112

117-
message = str(exc)
118-
raise mapped_exc(message) from exc
113+
return mapped_exc(str(exc))
119114

120115

121116
class ResponseStream(SyncByteStream):
122117
def __init__(self, httpcore_stream: typing.Iterable[bytes]) -> None:
123118
self._httpcore_stream = httpcore_stream
124119

125120
def __iter__(self) -> typing.Iterator[bytes]:
126-
with map_httpcore_exceptions():
121+
try:
127122
for part in self._httpcore_stream:
128123
yield part
124+
except _get_httpcore_exception_types() as exc:
125+
raise _map_httpcore_exception(exc) from exc
129126

130127
def close(self) -> None:
131128
if hasattr(self._httpcore_stream, "close"):
@@ -224,8 +221,10 @@ def __exit__(
224221
exc_value: BaseException | None = None,
225222
traceback: TracebackType | None = None,
226223
) -> None:
227-
with map_httpcore_exceptions():
224+
try:
228225
self._pool.__exit__(exc_type, exc_value, traceback)
226+
except _get_httpcore_exception_types() as exc: # pragma: no cover
227+
raise _map_httpcore_exception(exc) from exc
229228

230229
def handle_request(
231230
self,
@@ -246,8 +245,10 @@ def handle_request(
246245
content=request.stream,
247246
extensions=request.extensions,
248247
)
249-
with map_httpcore_exceptions():
248+
try:
250249
resp = self._pool.handle_request(req)
250+
except _get_httpcore_exception_types() as exc:
251+
raise _map_httpcore_exception(exc) from exc
251252

252253
assert isinstance(resp.stream, typing.Iterable)
253254

@@ -267,9 +268,11 @@ def __init__(self, httpcore_stream: typing.AsyncIterable[bytes]) -> None:
267268
self._httpcore_stream = httpcore_stream
268269

269270
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
270-
with map_httpcore_exceptions():
271+
try:
271272
async for part in self._httpcore_stream:
272273
yield part
274+
except _get_httpcore_exception_types() as exc: # pragma: no cover
275+
raise _map_httpcore_exception(exc) from exc
273276

274277
async def aclose(self) -> None:
275278
if hasattr(self._httpcore_stream, "aclose"):
@@ -368,8 +371,10 @@ async def __aexit__(
368371
exc_value: BaseException | None = None,
369372
traceback: TracebackType | None = None,
370373
) -> None:
371-
with map_httpcore_exceptions():
374+
try:
372375
await self._pool.__aexit__(exc_type, exc_value, traceback)
376+
except _get_httpcore_exception_types() as exc: # pragma: no cover
377+
raise _map_httpcore_exception(exc) from exc
373378

374379
async def handle_async_request(
375380
self,
@@ -390,8 +395,10 @@ async def handle_async_request(
390395
content=request.stream,
391396
extensions=request.extensions,
392397
)
393-
with map_httpcore_exceptions():
398+
try:
394399
resp = await self._pool.handle_async_request(req)
400+
except _get_httpcore_exception_types() as exc:
401+
raise _map_httpcore_exception(exc) from exc
395402

396403
assert isinstance(resp.stream, typing.AsyncIterable)
397404

0 commit comments

Comments
 (0)