Skip to content

Commit 412cb67

Browse files
veeceeyclaude
andauthored
fix: close HTTPError to prevent ResourceWarning on Python 3.14 (#1133)
* fix: close HTTPError response to prevent ResourceWarning on Python 3.14 On Python 3.14, urllib.error.HTTPError objects that are not explicitly closed produce a ResourceWarning during garbage collection. Since HTTPError is both a URLError subclass and a response-like object (inheriting from tempfile._TemporaryFileWrapper via addinfourl), it must be closed to release the underlying resource. This adds an explicit e.close() call in the except handler of PyJWKClient.fetch_data() when the caught exception is an HTTPError. Fixes #1128 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: use BytesIO for HTTPError fp in test to fix Python 3.9 compatibility On Python 3.9, HTTPError(fp=None).close() triggers a KeyError in tempfile.SpooledTemporaryFile.__getattr__ because the internal 'file' dict key doesn't exist. Using io.BytesIO(b"") as the fp argument provides a valid file-like object that can be closed on all Python versions. Also adds CHANGELOG.rst entry for the HTTPError close fix. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent 53e9381 commit 412cb67

3 files changed

Lines changed: 30 additions & 2 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Fixed
1111
~~~~~
1212

1313
- Annotate PyJWKSet.keys for pyright by @tamird in `#1134 <https://github.com/jpadilla/pyjwt/pull/1134>`__
14+
- Close ``HTTPError`` response to prevent ``ResourceWarning`` on Python 3.14 by @veeceey in `#1133 <https://github.com/jpadilla/pyjwt/pull/1133>`__
1415

1516
Added
1617
~~~~~

jwt/jwks_client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from functools import lru_cache
66
from ssl import SSLContext
77
from typing import Any
8-
from urllib.error import URLError
8+
from urllib.error import HTTPError, URLError
99

1010
from .api_jwk import PyJWK, PyJWKSet
1111
from .api_jwt import decode_complete as decode_token
@@ -110,6 +110,8 @@ def fetch_data(self) -> Any:
110110
) as response:
111111
jwk_set = json.load(response)
112112
except (URLError, TimeoutError) as e:
113+
if isinstance(e, HTTPError):
114+
e.close()
113115
raise PyJWKClientConnectionError(
114116
f'Fail to fetch data from the url, err: "{e}"'
115117
) from e

tests/test_jwks_client.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import contextlib
2+
import io
23
import json
34
import ssl
45
import time
56
from unittest import mock
6-
from urllib.error import URLError
7+
from urllib.error import HTTPError, URLError
78

89
import pytest
910

@@ -86,6 +87,20 @@ def mocked_timeout():
8687
yield urlopen_mock
8788

8889

90+
@contextlib.contextmanager
91+
def mocked_http_error_response():
92+
with mock.patch("urllib.request.urlopen") as urlopen_mock:
93+
http_error = HTTPError(
94+
url="https://example.com",
95+
code=401,
96+
msg="Unauthorized",
97+
hdrs=None,
98+
fp=io.BytesIO(b""),
99+
)
100+
urlopen_mock.side_effect = http_error
101+
yield urlopen_mock, http_error
102+
103+
89104
@crypto_required
90105
class TestPyJWKClient:
91106
def test_fetch_data_forwards_headers_to_correct_url(self):
@@ -361,3 +376,13 @@ def test_get_jwt_set_sslcontext_no_ca(self):
361376
)
362377
with pytest.raises(PyJWKClientError):
363378
jwks_client.get_jwk_set()
379+
380+
def test_http_error_is_closed_on_connection_failure(self):
381+
url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json"
382+
jwks_client = PyJWKClient(url)
383+
384+
with mocked_http_error_response() as (_, http_error):
385+
with pytest.raises(PyJWKClientConnectionError):
386+
jwks_client.get_jwk_set()
387+
388+
assert http_error.closed

0 commit comments

Comments
 (0)