From 9542a1783114401146546bfaab03ef50c830f5ff Mon Sep 17 00:00:00 2001 From: Daniel Saxton <2658661+dsaxton@users.noreply.github.com> Date: Sat, 6 Feb 2021 17:23:30 -0600 Subject: [PATCH 01/10] Fix doc capitalization (#1460) --- docs/environment_variables.md | 4 ++-- httpx/_status_codes.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/environment_variables.md b/docs/environment_variables.md index 525c525117..1cc5ff3437 100644 --- a/docs/environment_variables.md +++ b/docs/environment_variables.md @@ -110,7 +110,7 @@ CLIENT_TRAFFIC_SECRET_0 XXXX Valid values: a filename -if this environment variable is set then HTTPX will load +If this environment variable is set then HTTPX will load CA certificate from the specified file instead of the default location. @@ -124,7 +124,7 @@ SSL_CERT_FILE=/path/to/ca-certs/ca-bundle.crt python -c "import httpx; httpx.get Valid values: a directory -if this environment variable is set then HTTPX will load +If this environment variable is set then HTTPX will load CA certificates from the specified location instead of the default location. diff --git a/httpx/_status_codes.py b/httpx/_status_codes.py index 71cf5cee98..f7ee6b64a9 100644 --- a/httpx/_status_codes.py +++ b/httpx/_status_codes.py @@ -26,7 +26,7 @@ def __new__(cls, value: int, phrase: str = "") -> "codes": obj = int.__new__(cls, value) # type: ignore obj._value_ = value - obj.phrase = phrase + obj.phrase = phrase # type: ignore return obj def __str__(self) -> str: From c52e7d212fa5b1e2525ccf84b2884ebeba354f36 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sat, 6 Feb 2021 19:48:23 -0800 Subject: [PATCH 02/10] Added missing Request __init__ parameters (#1456) Co-authored-by: Joe --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 817467ec55..0a5afd1fa9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -97,7 +97,7 @@ what gets sent over the wire.* >>> response = client.send(request) ``` -* `def __init__(method, url, [params], [data], [json], [headers], [cookies])` +* `def __init__(method, url, [params], [headers], [cookies], [content], [data], [files], [json], [stream])` * `.method` - **str** * `.url` - **URL** * `.content` - **byte**, **byte iterator**, or **byte async iterator** From 28478694759edd882e95398af8b60f033019f623 Mon Sep 17 00:00:00 2001 From: David Bordeynik Date: Sun, 7 Feb 2021 10:33:45 +0200 Subject: [PATCH 03/10] fix-1457: URL's full_path -> raw_path from pull #1285 in docs as well (#1458) --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 0a5afd1fa9..c644f8a311 100644 --- a/docs/api.md +++ b/docs/api.md @@ -121,7 +121,7 @@ what gets sent over the wire.* * `.port` - **int** * `.path` - **str** * `.query` - **str** -* `.full_path` - **str** +* `.raw_path` - **str** * `.fragment` - **str** * `.is_ssl` - **bool** * `.is_absolute_url` - **bool** From d964343fa14852bcf475a83ef9774f8c388317d7 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 9 Feb 2021 14:10:42 +0000 Subject: [PATCH 04/10] Add Changelog link to PyPI page (#1462) Makes it easy to find out about changes, and it gets a little "present" icon, for example https://pypi.org/project/django-linear-migrations/ --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 075673ee6f..0c59851011 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ def get_packages(package): version=get_version("httpx"), url="https://github.com/encode/httpx", project_urls={ + "Changelog": "https://github.com/encode/httpx/blob/master/CHANGELOG.md", "Documentation": "https://www.python-httpx.org", "Source": "https://github.com/encode/httpx", }, From 02a692aba574a8e1ea7abd6fdfbfa77258ad8e6e Mon Sep 17 00:00:00 2001 From: Aber Date: Tue, 16 Feb 2021 20:33:17 +0800 Subject: [PATCH 05/10] Handle default ports in WSGITransport (#1469) * Maybe port is `None` https://www.python.org/dev/peps/pep-3333/#environ-variables > SERVER_NAME, SERVER_PORT > When HTTP_HOST is not set, these variables can be combined to determine a default. See the URL Reconstruction section below for more detail. SERVER_NAME and SERVER_PORT are required strings and must never be empty. * Add unit test * Compute default port Co-authored-by: Florimond Manca --- httpx/_transports/wsgi.py | 3 +++ tests/test_wsgi.py | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/httpx/_transports/wsgi.py b/httpx/_transports/wsgi.py index 953e1908c2..67b44bde42 100644 --- a/httpx/_transports/wsgi.py +++ b/httpx/_transports/wsgi.py @@ -74,6 +74,9 @@ def request( scheme, host, port, full_path = url path, _, query = full_path.partition(b"?") + if port is None: + port = {b"http": 80, b"https": 443}[scheme] + environ = { "wsgi.version": (1, 0), "wsgi.url_scheme": scheme.decode("ascii"), diff --git a/tests/test_wsgi.py b/tests/test_wsgi.py index 7bf11a0c34..b130e53c40 100644 --- a/tests/test_wsgi.py +++ b/tests/test_wsgi.py @@ -116,3 +116,30 @@ def test_wsgi_generator_empty(): response = client.get("http://www.example.org/") assert response.status_code == 200 assert response.text == "" + + +@pytest.mark.parametrize( + "url, expected_server_port", + [ + pytest.param("http://www.example.org", "80", id="auto-http"), + pytest.param("https://www.example.org", "443", id="auto-https"), + pytest.param("http://www.example.org:8000", "8000", id="explicit-port"), + ], +) +def test_wsgi_server_port(url: str, expected_server_port: int): + """ + SERVER_PORT is populated correctly from the requested URL. + """ + hello_world_app = application_factory([b"Hello, World!"]) + server_port: str + + def app(environ, start_response): + nonlocal server_port + server_port = environ["SERVER_PORT"] + return hello_world_app(environ, start_response) + + client = httpx.Client(app=app) + response = client.get(url) + assert response.status_code == 200 + assert response.text == "Hello, World!" + assert server_port == expected_server_port From 5af6ab0038ac3b6e1024a76853ff2848169b4aa8 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Wed, 17 Feb 2021 14:06:28 +0400 Subject: [PATCH 06/10] Explain REQUESTS_CA_BUNDLE migration (#1471) --- docs/compatibility.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/compatibility.md b/docs/compatibility.md index c8a074e491..99faf43232 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -78,6 +78,8 @@ When using a `Client` instance, the `trust_env`, `verify`, and `cert` arguments If you need more than one different SSL configuration, you should use different client instances for each SSL configuration. +Requests supports `REQUESTS_CA_BUNDLE` which points to either a file or a directory. HTTPX supports the `SSL_CERT_FILE` (for a file) and `SSL_CERT_DIR` (for a directory) OpenSSL variables instead. + ## Request body on HTTP methods The HTTP `GET`, `DELETE`, `HEAD`, and `OPTIONS` methods are specified as not supporting a request body. To stay in line with this, the `.get`, `.delete`, `.head` and `.options` functions do not support `files`, `data`, or `json` arguments. From 645ae4ed9c5fe35788ca13b9bd9d264b2dfb2784 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Wed, 17 Feb 2021 14:11:08 +0400 Subject: [PATCH 07/10] Explain SSL_CERT_DIR specific format (#1470) It's easy to believe that any .pem files there will get picked up automatically, but that's not the case. --- docs/environment_variables.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/environment_variables.md b/docs/environment_variables.md index 1cc5ff3437..d9cc89a58f 100644 --- a/docs/environment_variables.md +++ b/docs/environment_variables.md @@ -122,11 +122,9 @@ SSL_CERT_FILE=/path/to/ca-certs/ca-bundle.crt python -c "import httpx; httpx.get ## `SSL_CERT_DIR` -Valid values: a directory +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 then HTTPX will load -CA certificates from the specified location instead of the default -location. +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: From 084f35648b64b70f7a8255faa196b09d432622fe Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Feb 2021 11:10:21 +0000 Subject: [PATCH 08/10] Allow handler to optionally be async when MockTransport is used with AsyncClient (#1449) --- httpx/_transports/mock.py | 11 +++++++++++ tests/client/test_async_client.py | 13 +++++++++++++ 2 files changed, 24 insertions(+) diff --git a/httpx/_transports/mock.py b/httpx/_transports/mock.py index 6f9ebc1e0f..a55a88b7a2 100644 --- a/httpx/_transports/mock.py +++ b/httpx/_transports/mock.py @@ -1,3 +1,4 @@ +import asyncio from typing import Callable, List, Optional, Tuple import httpcore @@ -47,7 +48,17 @@ async def arequest( stream=stream, ) await request.aread() + response = self.handler(request) + + # Allow handler to *optionally* be an `async` function. + # If it is, then the `response` variable need to be awaited to actually + # return the result. + + # https://simonwillison.net/2020/Sep/2/await-me-maybe/ + if asyncio.iscoroutine(response): + response = await response + return ( response.status_code, response.headers.raw, diff --git a/tests/client/test_async_client.py b/tests/client/test_async_client.py index 0cc9d8a4ff..62464865e2 100644 --- a/tests/client/test_async_client.py +++ b/tests/client/test_async_client.py @@ -301,3 +301,16 @@ async def test_mounted_transport(): response = await client.get("custom://www.example.com") assert response.status_code == 200 assert response.json() == {"app": "mounted"} + + +@pytest.mark.usefixtures("async_environment") +async def test_async_mock_transport(): + async def hello_world(request): + return httpx.Response(200, text="Hello, world!") + + transport = httpx.MockTransport(hello_world) + + async with httpx.AsyncClient(transport=transport) as client: + response = await client.get("https://www.example.com") + assert response.status_code == 200 + assert response.text == "Hello, world!" From bd4caa873c8b342231b4c71346363b8895a7a7b2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Feb 2021 11:27:10 +0000 Subject: [PATCH 09/10] Tweak `create_ssl_context` defaults. (#1447) * Version 0.17.0 * create_ssl_config uses trust_env=True default * Drop CHANGELOG notes, to be included in a version PR instead --- httpx/_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpx/_config.py b/httpx/_config.py index 45e93a9964..837519afb5 100644 --- a/httpx/_config.py +++ b/httpx/_config.py @@ -43,7 +43,7 @@ class UnsetType: def create_ssl_context( cert: CertTypes = None, verify: VerifyTypes = True, - trust_env: bool = None, + trust_env: bool = True, http2: bool = False, ) -> ssl.SSLContext: return SSLConfig( @@ -63,7 +63,7 @@ def __init__( *, cert: CertTypes = None, verify: VerifyTypes = True, - trust_env: bool = None, + trust_env: bool = True, http2: bool = False, ): self.cert = cert From 0f280af8b170ed5cc48c12a894f71a8b5762f748 Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Wed, 17 Feb 2021 06:32:43 -0500 Subject: [PATCH 10/10] map_exceptions in request.aclose() (#1465) * map_exceptions in request.aclose() --- httpx/_client.py | 3 ++- tests/client/test_async_client.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/httpx/_client.py b/httpx/_client.py index b300cccb1f..3465a10b75 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -1500,7 +1500,8 @@ async def _send_single_request( async def on_close(response: Response) -> None: response.elapsed = datetime.timedelta(seconds=await timer.async_elapsed()) if hasattr(stream, "aclose"): - await stream.aclose() + with map_exceptions(HTTPCORE_EXC_MAP, request=request): + await stream.aclose() response = Response( status_code, diff --git a/tests/client/test_async_client.py b/tests/client/test_async_client.py index 62464865e2..1d3f4ccafa 100644 --- a/tests/client/test_async_client.py +++ b/tests/client/test_async_client.py @@ -303,6 +303,25 @@ async def test_mounted_transport(): assert response.json() == {"app": "mounted"} +@pytest.mark.usefixtures("async_environment") +async def test_response_aclose_map_exceptions(): + class BrokenStream: + async def __aiter__(self): + # so we're an AsyncIterator + pass # pragma: nocover + + async def aclose(self): + raise httpcore.CloseError(OSError(104, "Connection reset by peer")) + + def handle(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, stream=BrokenStream()) + + async with httpx.AsyncClient(transport=httpx.MockTransport(handle)) as client: + async with client.stream("GET", "http://example.com") as response: + with pytest.raises(httpx.CloseError): + await response.aclose() + + @pytest.mark.usefixtures("async_environment") async def test_async_mock_transport(): async def hello_world(request):