From 189fc4bcbe5f314128775dec66a616ac9a31ad48 Mon Sep 17 00:00:00 2001 From: Bob Conan Date: Wed, 20 Nov 2024 06:27:29 -0600 Subject: [PATCH 1/9] Update CHANGELOG.md, fix typo(s) (#3406) --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7ad03c0..3c16d667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -617,7 +617,7 @@ See pull requests #1057, #1058. * Added dedicated exception class `httpx.HTTPStatusError` for `.raise_for_status()` exceptions. (Pull #1072) * Added `httpx.create_ssl_context()` helper function. (Pull #996) -* Support for proxy exlcusions like `proxies={"https://www.example.com": None}`. (Pull #1099) +* Support for proxy exclusions like `proxies={"https://www.example.com": None}`. (Pull #1099) * Support `QueryParams(None)` and `client.params = None`. (Pull #1060) ### Changed @@ -845,7 +845,7 @@ We believe the API is now pretty much stable, and are aiming for a 1.0 release s ### Fixed -- Fix issue with concurrent connection acquiry. (Pull #700) +- Fix issue with concurrent connection acquisition. (Pull #700) - Fix write error on closing HTTP/2 connections. (Pull #699) ## 0.10.0 (December 29th, 2019) @@ -1094,7 +1094,7 @@ importing modules within the package. ## 0.6.7 (July 8, 2019) -- Check for connection aliveness on re-acquiry (Pull #111) +- Check for connection aliveness on re-acquisition (Pull #111) ## 0.6.6 (July 3, 2019) From 47f4a96ffaaaa07dca1614409549b5d7a6e7af49 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 22 Nov 2024 11:42:51 +0000 Subject: [PATCH 2/9] Handle empty zstd responses (#3412) --- CHANGELOG.md | 2 +- README.md | 4 +--- httpx/__version__.py | 2 +- httpx/_decoders.py | 4 ++++ tests/test_decoders.py | 19 +++++++++++++++++++ 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c16d667..4e2afe2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [Unreleased] +## 0.28.0 (...) The 0.28 release includes a limited set of backwards incompatible changes. diff --git a/README.md b/README.md index d5d21487..23992d9c 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,7 @@

-HTTPX is a fully featured HTTP client library for Python 3. It includes **an integrated -command line client**, has support for both **HTTP/1.1 and HTTP/2**, and provides both **sync -and async APIs**. +HTTPX is a fully featured HTTP client library for Python 3. It includes **an integrated command line client**, has support for both **HTTP/1.1 and HTTP/2**, and provides both **sync and async APIs**. --- diff --git a/httpx/__version__.py b/httpx/__version__.py index 5eaaddba..0a684ac3 100644 --- a/httpx/__version__.py +++ b/httpx/__version__.py @@ -1,3 +1,3 @@ __title__ = "httpx" __description__ = "A next generation HTTP client, for Python 3." -__version__ = "0.27.2" +__version__ = "0.28.0" diff --git a/httpx/_decoders.py b/httpx/_decoders.py index 180898c5..899dfada 100644 --- a/httpx/_decoders.py +++ b/httpx/_decoders.py @@ -175,9 +175,11 @@ class ZStandardDecoder(ContentDecoder): ) from None self.decompressor = zstandard.ZstdDecompressor().decompressobj() + self.seen_data = False def decode(self, data: bytes) -> bytes: assert zstandard is not None + self.seen_data = True output = io.BytesIO() try: output.write(self.decompressor.decompress(data)) @@ -190,6 +192,8 @@ class ZStandardDecoder(ContentDecoder): return output.getvalue() def flush(self) -> bytes: + if not self.seen_data: + return b"" ret = self.decompressor.flush() # note: this is a no-op if not self.decompressor.eof: raise DecodingError("Zstandard data is incomplete") # pragma: no cover diff --git a/tests/test_decoders.py b/tests/test_decoders.py index bcbb18bb..9ffaba18 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -100,6 +100,25 @@ def test_zstd_decoding_error(): ) +def test_zstd_empty(): + headers = [(b"Content-Encoding", b"zstd")] + response = httpx.Response(200, headers=headers, content=b"") + assert response.content == b"" + + +def test_zstd_truncated(): + body = b"test 123" + compressed_body = zstd.compress(body) + + headers = [(b"Content-Encoding", b"zstd")] + with pytest.raises(httpx.DecodingError): + httpx.Response( + 200, + headers=headers, + content=compressed_body[1:3], + ) + + def test_zstd_multiframe(): # test inspired by urllib3 test suite data = ( From ce7e14da27abba6574be9b3ea7cd5990556a9343 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 28 Nov 2024 11:46:59 +0000 Subject: [PATCH 3/9] Error on verify as str. (#3418) --- httpx/_config.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/httpx/_config.py b/httpx/_config.py index 9318de3c..1dec1bd3 100644 --- a/httpx/_config.py +++ b/httpx/_config.py @@ -31,6 +31,14 @@ def create_ssl_context(verify: ssl.SSLContext | bool = True) -> ssl.SSLContext: ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE return ssl_context + elif isinstance(verify, str): # pragma: nocover + # Explicitly handle this deprecated usage pattern. + msg = ( + "verify should be a boolean or SSLContext, since version 0.28. " + "Use `verify=ssl.create_default_context(cafile=...)` " + "or `verify=ssl.create_default_context(capath=...)`." + ) + raise RuntimeError(msg) return verify From a33c87852b8a0dddc65e5f739af1e0a6fca4b91f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 28 Nov 2024 13:31:17 +0000 Subject: [PATCH 4/9] Fix `extensions` type annotation. (#3380) --- httpx/_models.py | 4 ++-- httpx/_types.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/httpx/_models.py b/httpx/_models.py index e7c992f7..67d74bf8 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -398,7 +398,7 @@ class Request: self.method = method.upper() self.url = URL(url) if params is None else URL(url, params=params) self.headers = Headers(headers) - self.extensions = {} if extensions is None else extensions + self.extensions = {} if extensions is None else dict(extensions) if cookies: Cookies(cookies).set_cookie_header(self) @@ -537,7 +537,7 @@ class Response: # the client will set `response.next_request`. self.next_request: Request | None = None - self.extensions: ResponseExtensions = {} if extensions is None else extensions + self.extensions = {} if extensions is None else dict(extensions) self.history = [] if history is None else list(history) self.is_closed = False diff --git a/httpx/_types.py b/httpx/_types.py index edd00da1..4f0eab96 100644 --- a/httpx/_types.py +++ b/httpx/_types.py @@ -15,7 +15,6 @@ from typing import ( Iterator, List, Mapping, - MutableMapping, Optional, Sequence, Tuple, @@ -67,7 +66,7 @@ AuthTypes = Union[ RequestContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]] ResponseContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]] -ResponseExtensions = MutableMapping[str, Any] +ResponseExtensions = Mapping[str, Any] RequestData = Mapping[str, Any] @@ -84,7 +83,7 @@ FileTypes = Union[ ] RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]] -RequestExtensions = MutableMapping[str, Any] +RequestExtensions = Mapping[str, Any] __all__ = ["AsyncByteStream", "SyncByteStream"] From 80960fa31918d7663c3f4c3ad61661cf0e80628f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 28 Nov 2024 14:50:04 +0000 Subject: [PATCH 5/9] Version 0.28.0. (#3419) --- CHANGELOG.md | 20 ++++++++-------- httpx/_api.py | 18 +++++++-------- httpx/_client.py | 41 ++++++++++++++++++++++++++------ httpx/_config.py | 45 ++++++++++++++++++++++++++++-------- httpx/_transports/default.py | 14 +++++++---- httpx/_types.py | 1 + 6 files changed, 98 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e2afe2e..bc3fa411 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,28 +4,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## 0.28.0 (...) +## 0.28.0 (28th November, 2024) -The 0.28 release includes a limited set of backwards incompatible changes. +The 0.28 release includes a limited set of deprecations. -**Backwards incompatible changes**: +**Deprecations**: -SSL configuration has been significantly simplified. +We are working towards a simplified SSL configuration API. -* The `verify` argument no longer accepts string arguments. -* The `cert` argument has now been removed. -* The `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables are no longer automatically used. +*For users of the standard `verify=True` or `verify=False` cases, or `verify=` case this should require no changes. The following cases have been deprecated...* -For users of the standard `verify=True` or `verify=False` cases this should require no changes. +* The `verify` argument as a string argument is now deprecated and will raise warnings. +* The `cert` argument is now deprecated and will raise warnings. -For information on configuring more complex SSL cases, please see the [SSL documentation](docs/advanced/ssl.md). +Our revised [SSL documentation](docs/advanced/ssl.md) covers how to implement the same behaviour with a more constrained API. **The following changes are also included**: -* The undocumented `URL.raw` property has now been deprecated, and will raise warnings. * The deprecated `proxies` argument has now been removed. * The deprecated `app` argument has now been removed. -* Ensure JSON request bodies are compact. (#3363) +* JSON request bodies use a compact representation. (#3363) * Review URL percent escape sets, based on WHATWG spec. (#3371, #3373) * Ensure `certifi` and `httpcore` are only imported if required. (#3377) * Treat `socks5h` as a valid proxy scheme. (#3178) diff --git a/httpx/_api.py b/httpx/_api.py index ab1be081..c3cda1ec 100644 --- a/httpx/_api.py +++ b/httpx/_api.py @@ -51,7 +51,7 @@ def request( proxy: ProxyTypes | None = None, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, follow_redirects: bool = False, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, trust_env: bool = True, ) -> Response: """ @@ -136,7 +136,7 @@ def stream( proxy: ProxyTypes | None = None, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, follow_redirects: bool = False, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, trust_env: bool = True, ) -> typing.Iterator[Response]: """ @@ -180,7 +180,7 @@ def get( auth: AuthTypes | None = None, proxy: ProxyTypes | None = None, follow_redirects: bool = False, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, trust_env: bool = True, ) -> Response: @@ -216,7 +216,7 @@ def options( auth: AuthTypes | None = None, proxy: ProxyTypes | None = None, follow_redirects: bool = False, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, trust_env: bool = True, ) -> Response: @@ -252,7 +252,7 @@ def head( auth: AuthTypes | None = None, proxy: ProxyTypes | None = None, follow_redirects: bool = False, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, trust_env: bool = True, ) -> Response: @@ -292,7 +292,7 @@ def post( auth: AuthTypes | None = None, proxy: ProxyTypes | None = None, follow_redirects: bool = False, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, trust_env: bool = True, ) -> Response: @@ -333,7 +333,7 @@ def put( auth: AuthTypes | None = None, proxy: ProxyTypes | None = None, follow_redirects: bool = False, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, trust_env: bool = True, ) -> Response: @@ -374,7 +374,7 @@ def patch( auth: AuthTypes | None = None, proxy: ProxyTypes | None = None, follow_redirects: bool = False, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, trust_env: bool = True, ) -> Response: @@ -412,7 +412,7 @@ def delete( proxy: ProxyTypes | None = None, follow_redirects: bool = False, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, trust_env: bool = True, ) -> Response: """ diff --git a/httpx/_client.py b/httpx/_client.py index 76325c14..018d440c 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -33,6 +33,7 @@ from ._transports.default import AsyncHTTPTransport, HTTPTransport from ._types import ( AsyncByteStream, AuthTypes, + CertTypes, CookieTypes, HeaderTypes, ProxyTypes, @@ -644,7 +645,9 @@ class Client(BaseClient): params: QueryParamTypes | None = None, headers: HeaderTypes | None = None, cookies: CookieTypes | None = None, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, http1: bool = True, http2: bool = False, proxy: ProxyTypes | None = None, @@ -656,7 +659,6 @@ class Client(BaseClient): event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, base_url: URL | str = "", transport: BaseTransport | None = None, - trust_env: bool = True, default_encoding: str | typing.Callable[[bytes], str] = "utf-8", ) -> None: super().__init__( @@ -687,6 +689,8 @@ class Client(BaseClient): self._transport = self._init_transport( verify=verify, + cert=cert, + trust_env=trust_env, http1=http1, http2=http2, limits=limits, @@ -698,6 +702,8 @@ class Client(BaseClient): else self._init_proxy_transport( proxy, verify=verify, + cert=cert, + trust_env=trust_env, http1=http1, http2=http2, limits=limits, @@ -713,7 +719,9 @@ class Client(BaseClient): def _init_transport( self, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, http1: bool = True, http2: bool = False, limits: Limits = DEFAULT_LIMITS, @@ -724,6 +732,8 @@ class Client(BaseClient): return HTTPTransport( verify=verify, + cert=cert, + trust_env=trust_env, http1=http1, http2=http2, limits=limits, @@ -732,13 +742,17 @@ class Client(BaseClient): def _init_proxy_transport( self, proxy: Proxy, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, http1: bool = True, http2: bool = False, limits: Limits = DEFAULT_LIMITS, ) -> BaseTransport: return HTTPTransport( verify=verify, + cert=cert, + trust_env=trust_env, http1=http1, http2=http2, limits=limits, @@ -1345,7 +1359,8 @@ class AsyncClient(BaseClient): params: QueryParamTypes | None = None, headers: HeaderTypes | None = None, cookies: CookieTypes | None = None, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, http1: bool = True, http2: bool = False, proxy: ProxyTypes | None = None, @@ -1388,6 +1403,8 @@ class AsyncClient(BaseClient): self._transport = self._init_transport( verify=verify, + cert=cert, + trust_env=trust_env, http1=http1, http2=http2, limits=limits, @@ -1400,6 +1417,8 @@ class AsyncClient(BaseClient): else self._init_proxy_transport( proxy, verify=verify, + cert=cert, + trust_env=trust_env, http1=http1, http2=http2, limits=limits, @@ -1414,7 +1433,9 @@ class AsyncClient(BaseClient): def _init_transport( self, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, http1: bool = True, http2: bool = False, limits: Limits = DEFAULT_LIMITS, @@ -1425,6 +1446,8 @@ class AsyncClient(BaseClient): return AsyncHTTPTransport( verify=verify, + cert=cert, + trust_env=trust_env, http1=http1, http2=http2, limits=limits, @@ -1433,13 +1456,17 @@ class AsyncClient(BaseClient): def _init_proxy_transport( self, proxy: Proxy, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, http1: bool = True, http2: bool = False, limits: Limits = DEFAULT_LIMITS, ) -> AsyncBaseTransport: return AsyncHTTPTransport( verify=verify, + cert=cert, + trust_env=trust_env, http1=http1, http2=http2, limits=limits, diff --git a/httpx/_config.py b/httpx/_config.py index 1dec1bd3..dbd2b46c 100644 --- a/httpx/_config.py +++ b/httpx/_config.py @@ -1,9 +1,10 @@ from __future__ import annotations +import os import typing from ._models import Headers -from ._types import HeaderTypes, TimeoutTypes +from ._types import CertTypes, HeaderTypes, TimeoutTypes from ._urls import URL if typing.TYPE_CHECKING: @@ -19,28 +20,54 @@ class UnsetType: UNSET = UnsetType() -def create_ssl_context(verify: ssl.SSLContext | bool = True) -> ssl.SSLContext: +def create_ssl_context( + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, +) -> ssl.SSLContext: import ssl + import warnings import certifi if verify is True: - return ssl.create_default_context(cafile=certifi.where()) + if trust_env and os.environ.get("SSL_CERT_FILE"): # pragma: nocover + ctx = ssl.create_default_context(cafile=os.environ["SSL_CERT_FILE"]) + elif trust_env and os.environ.get("SSL_CERT_DIR"): # pragma: nocover + ctx = ssl.create_default_context(capath=os.environ["SSL_CERT_DIR"]) + else: + # Default case... + ctx = ssl.create_default_context(cafile=certifi.where()) elif verify is False: ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE return ssl_context elif isinstance(verify, str): # pragma: nocover - # Explicitly handle this deprecated usage pattern. - msg = ( - "verify should be a boolean or SSLContext, since version 0.28. " + message = ( + "`verify=` is deprecated. " "Use `verify=ssl.create_default_context(cafile=...)` " - "or `verify=ssl.create_default_context(capath=...)`." + "or `verify=ssl.create_default_context(capath=...)` instead." ) - raise RuntimeError(msg) + warnings.warn(message, DeprecationWarning) + if os.path.isdir(verify): + return ssl.create_default_context(capath=verify) + return ssl.create_default_context(cafile=verify) + else: + ctx = verify - return verify + if cert: # pragma: nocover + message = ( + "`cert=...` is deprecated. Use `verify=` instead," + "with `.load_cert_chain()` to configure the certificate chain." + ) + warnings.warn(message, DeprecationWarning) + if isinstance(cert, str): + ctx.load_cert_chain(cert) + else: + ctx.load_cert_chain(*cert) + + return ctx class Timeout: diff --git a/httpx/_transports/default.py b/httpx/_transports/default.py index ccc19af4..d5aa05ff 100644 --- a/httpx/_transports/default.py +++ b/httpx/_transports/default.py @@ -53,7 +53,7 @@ from .._exceptions import ( WriteTimeout, ) from .._models import Request, Response -from .._types import AsyncByteStream, ProxyTypes, SyncByteStream +from .._types import AsyncByteStream, CertTypes, ProxyTypes, SyncByteStream from .._urls import URL from .base import AsyncBaseTransport, BaseTransport @@ -135,7 +135,9 @@ class ResponseStream(SyncByteStream): class HTTPTransport(BaseTransport): def __init__( self, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, http1: bool = True, http2: bool = False, limits: Limits = DEFAULT_LIMITS, @@ -148,7 +150,7 @@ class HTTPTransport(BaseTransport): import httpcore proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy - ssl_context = create_ssl_context(verify=verify) + ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) if proxy is None: self._pool = httpcore.ConnectionPool( @@ -277,7 +279,9 @@ class AsyncResponseStream(AsyncByteStream): class AsyncHTTPTransport(AsyncBaseTransport): def __init__( self, - verify: ssl.SSLContext | bool = True, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, http1: bool = True, http2: bool = False, limits: Limits = DEFAULT_LIMITS, @@ -290,7 +294,7 @@ class AsyncHTTPTransport(AsyncBaseTransport): import httpcore proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy - ssl_context = create_ssl_context(verify=verify) + ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) if proxy is None: self._pool = httpcore.AsyncConnectionPool( diff --git a/httpx/_types.py b/httpx/_types.py index 4f0eab96..704dfdff 100644 --- a/httpx/_types.py +++ b/httpx/_types.py @@ -57,6 +57,7 @@ TimeoutTypes = Union[ "Timeout", ] ProxyTypes = Union["URL", str, "Proxy"] +CertTypes = Union[str, Tuple[str, str], Tuple[str, str, str]] AuthTypes = Union[ Tuple[Union[str, bytes], Union[str, bytes]], From 15e21e9ea3cad4f06e22a7e704aabefdf43d2e29 Mon Sep 17 00:00:00 2001 From: Daniel Arvelini Date: Fri, 29 Nov 2024 08:15:56 -0300 Subject: [PATCH 6/9] Updating deprecated docstring Client() class (#3426) --- httpx/_client.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/httpx/_client.py b/httpx/_client.py index 018d440c..2249231f 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -620,8 +620,6 @@ class Client(BaseClient): * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be enabled. Defaults to `False`. * **proxy** - *(optional)* A proxy URL where all the traffic should be routed. - * **proxies** - *(optional)* A dictionary mapping proxy keys to proxy - URLs. * **timeout** - *(optional)* The timeout configuration to use when sending requests. * **limits** - *(optional)* The limits configuration to use. From 0cb7e5a2e736628e2f506d259fcf0d48cd2bde82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 08:37:45 +0100 Subject: [PATCH 7/9] Bump the python-packages group with 11 updates (#3434) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Marcelo Trylesinski --- requirements.txt | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index 53fd0a6c..0d8ba2ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,19 +11,20 @@ chardet==5.2.0 # Documentation mkdocs==1.6.1 mkautodoc==0.2.0 -mkdocs-material==9.5.39 +mkdocs-material==9.5.47 # Packaging -build==1.2.2 -twine==5.1.1 +build==1.2.2.post1 +twine==6.0.1 # Tests & Linting coverage[toml]==7.6.1 -cryptography==43.0.1 -mypy==1.11.2 -pytest==8.3.3 -ruff==0.6.8 -trio==0.26.2 +cryptography==44.0.0 +mypy==1.13.0 +pytest==8.3.4 +ruff==0.8.1 +trio==0.27.0 trio-typing==0.10.0 -trustme==1.1.0 -uvicorn==0.31.0 +trustme==1.1.0; python_version < '3.9' +trustme==1.2.0; python_version >= '3.9' +uvicorn==0.32.1 From 8ecb86f0d74ffc52d4663214fae9526bee89358d Mon Sep 17 00:00:00 2001 From: Elaina Date: Wed, 4 Dec 2024 00:12:27 +0800 Subject: [PATCH 8/9] Add test for request params behavior changes (#3364) (#3440) Co-authored-by: Tom Christie --- CHANGELOG.md | 1 + tests/models/test_requests.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc3fa411..060013b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Our revised [SSL documentation](docs/advanced/ssl.md) covers how to implement th * Ensure `certifi` and `httpcore` are only imported if required. (#3377) * Treat `socks5h` as a valid proxy scheme. (#3178) * Cleanup `Request()` method signature in line with `client.request()` and `httpx.request()`. (#3378) +* Bugfix: When passing `params={}`, always strictly update rather than merge with an existing querystring. (#3364) ## 0.27.2 (27th August, 2024) diff --git a/tests/models/test_requests.py b/tests/models/test_requests.py index d2a458d5..b31fe007 100644 --- a/tests/models/test_requests.py +++ b/tests/models/test_requests.py @@ -226,3 +226,16 @@ def test_request_generator_content_picklable(): request.read() pickle_request = pickle.loads(pickle.dumps(request)) assert pickle_request.content == b"test 123" + + +def test_request_params(): + request = httpx.Request("GET", "http://example.com", params={}) + assert str(request.url) == "http://example.com" + + request = httpx.Request( + "GET", "http://example.com?c=3", params={"a": "1", "b": "2"} + ) + assert str(request.url) == "http://example.com?a=1&b=2" + + request = httpx.Request("GET", "http://example.com?a=1", params={}) + assert str(request.url) == "http://example.com" From 89599a9541af14bcf906fc4ed58ccbdf403802ba Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 4 Dec 2024 11:29:09 +0000 Subject: [PATCH 9/9] Fix `verify=False`, `cert=...` case. (#3442) --- CHANGELOG.md | 4 ++++ httpx/_config.py | 7 +++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 060013b0..4f1233ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## Dev + +* Fix SSL case where `verify=False` together with client side certificates. + ## 0.28.0 (28th November, 2024) The 0.28 release includes a limited set of deprecations. diff --git a/httpx/_config.py b/httpx/_config.py index dbd2b46c..467a6c90 100644 --- a/httpx/_config.py +++ b/httpx/_config.py @@ -39,10 +39,9 @@ def create_ssl_context( # Default case... ctx = ssl.create_default_context(cafile=certifi.where()) elif verify is False: - ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - return ssl_context + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE elif isinstance(verify, str): # pragma: nocover message = ( "`verify=` is deprecated. "