From 364697efcafe39290358745da8be451a40a3eab2 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 3 Sep 2025 13:17:26 +0200 Subject: [PATCH 01/13] Upgrade Python formatter ruff (#3651) --- httpx/_exceptions.py | 4 +--- httpx/_urls.py | 2 +- requirements.txt | 2 +- tests/test_content.py | 12 ++++++------ 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/httpx/_exceptions.py b/httpx/_exceptions.py index 77f45a6d..dd7fb6cd 100644 --- a/httpx/_exceptions.py +++ b/httpx/_exceptions.py @@ -331,9 +331,7 @@ class StreamClosed(StreamError): """ def __init__(self) -> None: - message = ( - "Attempted to read or stream content, but the stream has " "been closed." - ) + message = "Attempted to read or stream content, but the stream has been closed." super().__init__(message) diff --git a/httpx/_urls.py b/httpx/_urls.py index 147a8fa3..301d0874 100644 --- a/httpx/_urls.py +++ b/httpx/_urls.py @@ -379,7 +379,7 @@ class URL: if ":" in userinfo: # Mask any password component. - userinfo = f'{userinfo.split(":")[0]}:[secure]' + userinfo = f"{userinfo.split(':')[0]}:[secure]" authority = "".join( [ diff --git a/requirements.txt b/requirements.txt index 646cb813..8b5a111a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ coverage[toml]==7.6.1 cryptography==44.0.1 mypy==1.13.0 pytest==8.3.4 -ruff==0.8.1 +ruff==0.12.11 trio==0.27.0 trio-typing==0.10.0 trustme==1.2.0 diff --git a/tests/test_content.py b/tests/test_content.py index f63ec18a..9bfe9837 100644 --- a/tests/test_content.py +++ b/tests/test_content.py @@ -489,18 +489,18 @@ def test_response_invalid_argument(): def test_ensure_ascii_false_with_french_characters(): data = {"greeting": "Bonjour, ça va ?"} response = httpx.Response(200, json=data) - assert ( - "ça va" in response.text - ), "ensure_ascii=False should preserve French accented characters" + assert "ça va" in response.text, ( + "ensure_ascii=False should preserve French accented characters" + ) assert response.headers["Content-Type"] == "application/json" def test_separators_for_compact_json(): data = {"clé": "valeur", "liste": [1, 2, 3]} response = httpx.Response(200, json=data) - assert ( - response.text == '{"clé":"valeur","liste":[1,2,3]}' - ), "separators=(',', ':') should produce a compact representation" + assert response.text == '{"clé":"valeur","liste":[1,2,3]}', ( + "separators=(',', ':') should produce a compact representation" + ) assert response.headers["Content-Type"] == "application/json" From 15e9759e656654b44307fbee685e032c7bb4aede Mon Sep 17 00:00:00 2001 From: Kamil Monicz Date: Thu, 4 Sep 2025 10:52:37 +0200 Subject: [PATCH 02/13] Add `httpx-secure` to third party packages (#3629) Co-authored-by: Kim Christie --- docs/third_party_packages.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/third_party_packages.md b/docs/third_party_packages.md index 546607f7..253c312f 100644 --- a/docs/third_party_packages.md +++ b/docs/third_party_packages.md @@ -24,6 +24,12 @@ Provides authentication classes to be used with HTTPX's [authentication paramete This package adds caching functionality to HTTPX +### httpx-secure + +[GitHub](https://github.com/Zaczero/httpx-secure) + +Drop-in SSRF protection for httpx with DNS caching and custom validation support. + ### httpx-socks [GitHub](https://github.com/romis2012/httpx-socks) From b55d4635701d9dc22928ee647880c76b078ba3f2 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 4 Sep 2025 15:48:49 +0200 Subject: [PATCH 03/13] Upgrade Python type checker mypy (#3654) --- requirements.txt | 2 +- tests/client/test_auth.py | 2 +- tests/client/test_properties.py | 12 ++++++------ tests/client/test_queryparams.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8b5a111a..18480596 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ twine==6.0.1 # Tests & Linting coverage[toml]==7.6.1 cryptography==44.0.1 -mypy==1.13.0 +mypy==1.17.1 pytest==8.3.4 ruff==0.12.11 trio==0.27.0 diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index 7638b8bd..72674e6f 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -326,7 +326,7 @@ async def test_auth_property() -> None: async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: assert client.auth is None - client.auth = ("user", "password123") # type: ignore + client.auth = ("user", "password123") assert isinstance(client.auth, httpx.BasicAuth) url = "https://example.org/" diff --git a/tests/client/test_properties.py b/tests/client/test_properties.py index eb870981..f9ca9f24 100644 --- a/tests/client/test_properties.py +++ b/tests/client/test_properties.py @@ -3,35 +3,35 @@ import httpx def test_client_base_url(): client = httpx.Client() - client.base_url = "https://www.example.org/" # type: ignore + client.base_url = "https://www.example.org/" assert isinstance(client.base_url, httpx.URL) assert client.base_url == "https://www.example.org/" def test_client_base_url_without_trailing_slash(): client = httpx.Client() - client.base_url = "https://www.example.org/path" # type: ignore + client.base_url = "https://www.example.org/path" assert isinstance(client.base_url, httpx.URL) assert client.base_url == "https://www.example.org/path/" def test_client_base_url_with_trailing_slash(): client = httpx.Client() - client.base_url = "https://www.example.org/path/" # type: ignore + client.base_url = "https://www.example.org/path/" assert isinstance(client.base_url, httpx.URL) assert client.base_url == "https://www.example.org/path/" def test_client_headers(): client = httpx.Client() - client.headers = {"a": "b"} # type: ignore + client.headers = {"a": "b"} assert isinstance(client.headers, httpx.Headers) assert client.headers["A"] == "b" def test_client_cookies(): client = httpx.Client() - client.cookies = {"a": "b"} # type: ignore + client.cookies = {"a": "b"} assert isinstance(client.cookies, httpx.Cookies) mycookies = list(client.cookies.jar) assert len(mycookies) == 1 @@ -42,7 +42,7 @@ def test_client_timeout(): expected_timeout = 12.0 client = httpx.Client() - client.timeout = expected_timeout # type: ignore + client.timeout = expected_timeout assert isinstance(client.timeout, httpx.Timeout) assert client.timeout.connect == expected_timeout diff --git a/tests/client/test_queryparams.py b/tests/client/test_queryparams.py index e5acb0ba..1c6d5873 100644 --- a/tests/client/test_queryparams.py +++ b/tests/client/test_queryparams.py @@ -17,7 +17,7 @@ def test_client_queryparams_string(): assert client.params["a"] == "b" client = httpx.Client() - client.params = "a=b" # type: ignore + client.params = "a=b" assert isinstance(client.params, httpx.QueryParams) assert client.params["a"] == "b" From 767cf6baa608a56d03f8fe438a39c2013904f0ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 15:29:43 +0100 Subject: [PATCH 04/13] Bump the python-packages group across 1 directory with 10 updates (#3658) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- requirements.txt | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index eb9e5c9a..fc3e95ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ brotli = [ cli = [ "click==8.*", "pygments==2.*", - "rich>=10,<14", + "rich>=10,<15", ] http2 = [ "h2>=3,<5", diff --git a/requirements.txt b/requirements.txt index 18480596..08953d82 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,19 +11,19 @@ chardet==5.2.0 # Documentation mkdocs==1.6.1 mkautodoc==0.2.0 -mkdocs-material==9.5.47 +mkdocs-material==9.6.18 # Packaging -build==1.2.2.post1 -twine==6.0.1 +build==1.3.0 +twine==6.1.0 # Tests & Linting -coverage[toml]==7.6.1 -cryptography==44.0.1 +coverage[toml]==7.10.6 +cryptography==45.0.7 mypy==1.17.1 -pytest==8.3.4 +pytest==8.4.1 ruff==0.12.11 -trio==0.27.0 +trio==0.30.0 trio-typing==0.10.0 -trustme==1.2.0 -uvicorn==0.32.1 +trustme==1.2.1 +uvicorn==0.35.0 From bc00d2bd9f715b3b848925e0c259887409bceabc Mon Sep 17 00:00:00 2001 From: Glen Keane Date: Fri, 5 Sep 2025 15:19:37 +0100 Subject: [PATCH 05/13] Update compatibility.md with documentation of exceptions differences (#3649) Co-authored-by: Kim Christie --- docs/compatibility.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/compatibility.md b/docs/compatibility.md index 52e9389a..96861675 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -226,3 +226,7 @@ For both query params (`params=`) and form data (`data=`), `requests` supports s In HTTPX, event hooks may access properties of requests and responses, but event hook callbacks cannot mutate the original request/response. If you are looking for more control, consider checking out [Custom Transports](advanced/transports.md#custom-transports). + +## Exceptions and Errors + +`requests` exception hierarchy is slightly different to the `httpx` exception hierarchy. `requests` exposes a top level `RequestException`, where as `httpx` exposes a top level `HTTPError`. see the exceptions exposes in requests [here](https://requests.readthedocs.io/en/latest/_modules/requests/exceptions/). See the `httpx` error hierarchy [here](https://www.python-httpx.org/exceptions/). From 3fee27838e9fd0cb528861e41f3ff98164081a4b Mon Sep 17 00:00:00 2001 From: nikkie Date: Fri, 5 Sep 2025 23:30:31 +0900 Subject: [PATCH 06/13] [docs] Remove load_ssl_context & load_verify_locations DEBUG log (#3589) Co-authored-by: Kim Christie --- docs/logging.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/logging.md b/docs/logging.md index 90c21e25..b3c57817 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -20,8 +20,6 @@ httpx.get("https://www.example.com") Will send debug level output to the console, or wherever `stdout` is directed too... ``` -DEBUG [2024-09-28 17:27:40] httpx - load_ssl_context verify=True cert=None -DEBUG [2024-09-28 17:27:40] httpx - load_verify_locations cafile='/Users/karenpetrosyan/oss/karhttpx/.venv/lib/python3.9/site-packages/certifi/cacert.pem' DEBUG [2024-09-28 17:27:40] httpcore.connection - connect_tcp.started host='www.example.com' port=443 local_address=None timeout=5.0 socket_options=None DEBUG [2024-09-28 17:27:41] httpcore.connection - connect_tcp.complete return_value= DEBUG [2024-09-28 17:27:41] httpcore.connection - start_tls.started ssl_context=SSLContext(verify=True) server_hostname='www.example.com' timeout=5.0 @@ -80,4 +78,4 @@ logging.config.dictConfig(LOGGING_CONFIG) httpx.get('https://www.example.com') ``` -The exact formatting of the debug logging may be subject to change across different versions of `httpx` and `httpcore`. If you need to rely on a particular format it is recommended that you pin installation of these packages to fixed versions. \ No newline at end of file +The exact formatting of the debug logging may be subject to change across different versions of `httpx` and `httpcore`. If you need to rely on a particular format it is recommended that you pin installation of these packages to fixed versions. From 652f051feaf1ab2828e1812eb0c48f9046a8b5ce Mon Sep 17 00:00:00 2001 From: Tobias Fischer <30701667+tobb10001@users.noreply.github.com> Date: Thu, 11 Sep 2025 10:59:20 +0000 Subject: [PATCH 07/13] Documentation for SSL_CERT_FILE and SSL_CERT_DIR (#3579) Co-authored-by: Kim Christie --- docs/advanced/ssl.md | 14 +------------- docs/environment_variables.md | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/docs/advanced/ssl.md b/docs/advanced/ssl.md index da40ed28..f61e82ce 100644 --- a/docs/advanced/ssl.md +++ b/docs/advanced/ssl.md @@ -71,19 +71,7 @@ client = httpx.Client(verify=ctx) ### Working with `SSL_CERT_FILE` and `SSL_CERT_DIR` -Unlike `requests`, the `httpx` package does not automatically pull in [the environment variables `SSL_CERT_FILE` or `SSL_CERT_DIR`](https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_default_verify_paths.html). If you want to use these they need to be enabled explicitly. - -For example... - -```python -# Use `SSL_CERT_FILE` or `SSL_CERT_DIR` if configured. -# Otherwise default to certifi. -ctx = ssl.create_default_context( - cafile=os.environ.get("SSL_CERT_FILE", certifi.where()), - capath=os.environ.get("SSL_CERT_DIR"), -) -client = httpx.Client(verify=ctx) -``` +`httpx` does respect the `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables by default. For details, refer to [the section on the environment variables page](../environment_variables.md#ssl_cert_file). ### Making HTTPS requests to a local server diff --git a/docs/environment_variables.md b/docs/environment_variables.md index 4f7a9f52..0364deb0 100644 --- a/docs/environment_variables.md +++ b/docs/environment_variables.md @@ -51,3 +51,29 @@ python -c "import httpx; httpx.get('http://example.com')" python -c "import httpx; httpx.get('http://127.0.0.1:5000/my-api')" python -c "import httpx; httpx.get('https://www.python-httpx.org')" ``` + +## `SSL_CERT_FILE` + +Valid values: a filename + +If this environment variable is set then HTTPX will load +CA certificate from the specified file instead of the default +location. + +Example: + +```console +SSL_CERT_FILE=/path/to/ca-certs/ca-bundle.crt python -c "import httpx; httpx.get('https://example.com')" +``` + +## `SSL_CERT_DIR` + +Valid values: a directory following an [OpenSSL specific layout](https://www.openssl.org/docs/manmaster/man3/SSL_CTX_load_verify_locations.html). + +If this environment variable is set and the directory follows an [OpenSSL specific layout](https://www.openssl.org/docs/manmaster/man3/SSL_CTX_load_verify_locations.html) (ie. you ran `c_rehash`) then HTTPX will load CA certificates from this directory instead of the default location. + +Example: + +```console +SSL_CERT_DIR=/path/to/ca-certs/ python -c "import httpx; httpx.get('https://example.com')" +``` From 4b23574cf83307ce27d3b14b4a425dc58c57d28d Mon Sep 17 00:00:00 2001 From: Kim Christie Date: Tue, 16 Sep 2025 14:23:31 +0100 Subject: [PATCH 08/13] Update dependencies (#3665) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 08953d82..ebc6ea7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ cryptography==45.0.7 mypy==1.17.1 pytest==8.4.1 ruff==0.12.11 -trio==0.30.0 +trio==0.31.0 trio-typing==0.10.0 trustme==1.2.1 uvicorn==0.35.0 From 435e1dac899adeb0c156c00721ecbb1124d75842 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 18:38:23 +0100 Subject: [PATCH 09/13] Bump actions/setup-python from 5 to 6 (#3677) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- .github/workflows/test-suite.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fb2304a8..a16f2587 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: "actions/checkout@v4" - - uses: "actions/setup-python@v5" + - uses: "actions/setup-python@v6" with: python-version: 3.9 - name: "Install dependencies" diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 9ea74686..92e8c360 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: "actions/checkout@v4" - - uses: "actions/setup-python@v5" + - uses: "actions/setup-python@v6" with: python-version: "${{ matrix.python-version }}" allow-prereleases: true From def4778d622e8bf49a9fea4dda78cca4cf666d8a Mon Sep 17 00:00:00 2001 From: ZProger <81753080+Zproger@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:04:38 +0200 Subject: [PATCH 10/13] Fixed a syntax error in the file upload example (#3692) --- docs/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 38da2fec..e140b53c 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -191,7 +191,7 @@ You can also explicitly set the filename and content type, by using a tuple of items for the file value: ```pycon ->>> with open('report.xls', 'rb') report_file: +>>> with open('report.xls', 'rb') as report_file: ... files = {'upload-file': ('report.xls', report_file, 'application/vnd.ms-excel')} ... r = httpx.post("https://httpbin.org/post", files=files) >>> print(r.text) From ca097c96f97d8d2a5da09b8ca736c7e78a2467f6 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 10 Dec 2025 15:47:31 +0100 Subject: [PATCH 11/13] docs/ssl: fix typo (#3703) --- docs/advanced/ssl.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/ssl.md b/docs/advanced/ssl.md index f61e82ce..3813293f 100644 --- a/docs/advanced/ssl.md +++ b/docs/advanced/ssl.md @@ -29,7 +29,7 @@ import certifi import httpx import ssl -# This SSL context is equivelent to the default `verify=True`. +# This SSL context is equivalent to the default `verify=True`. ctx = ssl.create_default_context(cafile=certifi.where()) client = httpx.Client(verify=ctx) ``` From ae1b9f66238f75ced3ced5e4485408435de10768 Mon Sep 17 00:00:00 2001 From: Josh Cannon Date: Wed, 10 Dec 2025 08:58:48 -0600 Subject: [PATCH 12/13] Expose `FunctionAuth` in `__all__` (#3699) Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com> Co-authored-by: Kar Petrosyan --- CHANGELOG.md | 4 ++++ httpx/__init__.py | 1 + httpx/_auth.py | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13bbfcdb..57fa44b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Drop support for Python 3.8 +### Added + +* Expose `FunctionAuth` from the public API. (#3699) + ## 0.28.1 (6th December, 2024) * Fix SSL case where `verify=False` together with client side certificates. diff --git a/httpx/__init__.py b/httpx/__init__.py index e9addde0..63225040 100644 --- a/httpx/__init__.py +++ b/httpx/__init__.py @@ -50,6 +50,7 @@ __all__ = [ "DecodingError", "delete", "DigestAuth", + "FunctionAuth", "get", "head", "Headers", diff --git a/httpx/_auth.py b/httpx/_auth.py index b03971ab..9d24faed 100644 --- a/httpx/_auth.py +++ b/httpx/_auth.py @@ -16,7 +16,7 @@ if typing.TYPE_CHECKING: # pragma: no cover from hashlib import _Hash -__all__ = ["Auth", "BasicAuth", "DigestAuth", "NetRCAuth"] +__all__ = ["Auth", "BasicAuth", "DigestAuth", "FunctionAuth", "NetRCAuth"] class Auth: From b5addb64f0161ff6bfe94c124ef76f6a1fba5254 Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Mon, 23 Feb 2026 10:40:42 +0000 Subject: [PATCH 13/13] Adapt test_response_decode_text_using_autodetect for chardet 6.0 (#3773) --- tests/models/test_responses.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/models/test_responses.py b/tests/models/test_responses.py index 06c28e1e..d2972da5 100644 --- a/tests/models/test_responses.py +++ b/tests/models/test_responses.py @@ -1011,7 +1011,10 @@ def test_response_decode_text_using_autodetect(): assert response.status_code == 200 assert response.reason_phrase == "OK" - assert response.encoding == "ISO-8859-1" + # The encoded byte string is consistent with either ISO-8859-1 or + # WINDOWS-1252. Versions <6.0 of chardet claim the former, while chardet + # 6.0 detects the latter. + assert response.encoding in ("ISO-8859-1", "WINDOWS-1252") assert response.text == text