From 28cc905c376e933e3f71f068fd2f2b6a20fccf60 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Wed, 24 Jun 2026 15:05:43 +0200 Subject: [PATCH] read _expire_at only once --- httpcore/_async/http11.py | 7 ++++++- httpcore/_async/http2.py | 7 ++++++- httpcore/_sync/http11.py | 7 ++++++- httpcore/_sync/http2.py | 7 ++++++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/httpcore/_async/http11.py b/httpcore/_async/http11.py index e6d6d709..94a14749 100644 --- a/httpcore/_async/http11.py +++ b/httpcore/_async/http11.py @@ -273,7 +273,12 @@ def is_available(self) -> bool: def has_expired(self) -> bool: now = time.monotonic() - keepalive_expired = self._expire_at is not None and now > self._expire_at + # Read `_expire_at` once into a local: another thread may reset it to + # `None` (see `handle_async_request`) between the `is not None` check and + # the comparison, which raises a `TypeError` on the free-threaded build + # where there is no GIL to serialise the two attribute reads. + expire_at = self._expire_at + keepalive_expired = expire_at is not None and now > expire_at # If the HTTP connection is idle but the socket is readable, then the # only valid state is that the socket is about to return b"", indicating diff --git a/httpcore/_async/http2.py b/httpcore/_async/http2.py index dbd0beeb..938cd317 100644 --- a/httpcore/_async/http2.py +++ b/httpcore/_async/http2.py @@ -521,7 +521,12 @@ def is_available(self) -> bool: def has_expired(self) -> bool: now = time.monotonic() - return self._expire_at is not None and now > self._expire_at + # Read `_expire_at` once into a local: another thread may reset it to + # `None` (see `handle_async_request`) between the `is not None` check and + # the comparison, which raises a `TypeError` on the free-threaded build + # where there is no GIL to serialise the two attribute reads. + expire_at = self._expire_at + return expire_at is not None and now > expire_at def is_idle(self) -> bool: return self._state == HTTPConnectionState.IDLE diff --git a/httpcore/_sync/http11.py b/httpcore/_sync/http11.py index ebd3a974..e654688e 100644 --- a/httpcore/_sync/http11.py +++ b/httpcore/_sync/http11.py @@ -273,7 +273,12 @@ def is_available(self) -> bool: def has_expired(self) -> bool: now = time.monotonic() - keepalive_expired = self._expire_at is not None and now > self._expire_at + # Read `_expire_at` once into a local: another thread may reset it to + # `None` (see `handle_request`) between the `is not None` check and + # the comparison, which raises a `TypeError` on the free-threaded build + # where there is no GIL to serialise the two attribute reads. + expire_at = self._expire_at + keepalive_expired = expire_at is not None and now > expire_at # If the HTTP connection is idle but the socket is readable, then the # only valid state is that the socket is about to return b"", indicating diff --git a/httpcore/_sync/http2.py b/httpcore/_sync/http2.py index ddcc1890..f5a9fc3f 100644 --- a/httpcore/_sync/http2.py +++ b/httpcore/_sync/http2.py @@ -521,7 +521,12 @@ def is_available(self) -> bool: def has_expired(self) -> bool: now = time.monotonic() - return self._expire_at is not None and now > self._expire_at + # Read `_expire_at` once into a local: another thread may reset it to + # `None` (see `handle_request`) between the `is not None` check and + # the comparison, which raises a `TypeError` on the free-threaded build + # where there is no GIL to serialise the two attribute reads. + expire_at = self._expire_at + return expire_at is not None and now > expire_at def is_idle(self) -> bool: return self._state == HTTPConnectionState.IDLE