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/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/). 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')" +``` 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. 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) 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/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 646cb813..ebc6ea7f 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 -mypy==1.13.0 -pytest==8.3.4 -ruff==0.8.1 -trio==0.27.0 +coverage[toml]==7.10.6 +cryptography==45.0.7 +mypy==1.17.1 +pytest==8.4.1 +ruff==0.12.11 +trio==0.31.0 trio-typing==0.10.0 -trustme==1.2.0 -uvicorn==0.32.1 +trustme==1.2.1 +uvicorn==0.35.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" diff --git a/tests/test_content.py b/tests/test_content.py index 6c968a3f..b9c1b2b0 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"