From 7568fcf431036ac1be2bf3d0e6e5075d8814815b Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Mon, 26 Aug 2019 06:56:43 -0500 Subject: [PATCH 1/8] Add easier debug logging for users --- docs/environment_variables.md | 47 +++++++++++++++++++++++++++++++ httpx/dispatch/connection.py | 11 ++++++++ httpx/dispatch/connection_pool.py | 9 ++++++ httpx/dispatch/http11.py | 27 ++++++++++++++++++ httpx/dispatch/http2.py | 16 ++++++++++- httpx/models.py | 6 ++++ httpx/utils.py | 30 ++++++++++++++++++++ mkdocs.yml | 1 + setup.cfg | 2 +- tests/dispatch/utils.py | 2 +- 10 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 docs/environment_variables.md diff --git a/docs/environment_variables.md b/docs/environment_variables.md new file mode 100644 index 0000000000..bb55151461 --- /dev/null +++ b/docs/environment_variables.md @@ -0,0 +1,47 @@ +Environment Variables +===================== + +The HTTPX library can be configured via environment variables. +Here are a list of environment variables that HTTPX recognizes +and what function they serve: + +HTTPX_DEBUG +----------- + +Valid values: `1`, `true` + +If this environment variable is set to `1` or `true` then +logging will be turned on by default to stderr about low-level +details of the HTTP request and responses being sent and received. + +This can help you debug issues and see what's exactly being sent +over the wire and to which location. + +Example: + +```python +# test_script.py + +import httpx +client = httpx.Client() +client.request("GET", "https://google.com") +``` + +```console +user@host:~$ HTTPX_DEBUG=1 python test_script.py +20:54:17.585 - httpx.dispatch.connection_pool - acquire_connection origin=Origin(scheme='https' host='www.google.com' port=443) +20:54:17.585 - httpx.dispatch.connection_pool - new_connection connection=HTTPConnection(origin=Origin(scheme='https' host='www.google.com' port=443)) +20:54:17.590 - httpx.dispatch.connection - start_connect host='www.google.com' port=443 timeout=TimeoutConfig(timeout=5.0) +20:54:17.651 - httpx.dispatch.connection - connected http_version='HTTP/2' +20:54:17.651 - httpx.dispatch.http2 - send_headers stream_id=1 headers=[(b':method', b'GET'), (b':authority', b'www.google.com'), ...] +20:54:17.652 - httpx.dispatch.http2 - end_stream stream_id=1 +20:54:17.681 - httpx.dispatch.http2 - receive_event stream_id=0 event= +20:54:17.681 - httpx.dispatch.http2 - receive_event stream_id=0 event= +20:54:17.682 - httpx.dispatch.http2 - receive_event stream_id=0 event= +20:54:17.739 - httpx.dispatch.http2 - receive_event stream_id=1 event= +20:54:17.741 - httpx.dispatch.http2 - receive_event stream_id=1 event= +20:54:17.742 - httpx.dispatch.http2 - receive_event stream_id=1 event= +20:54:17.742 - httpx.dispatch.http2 - receive_event stream_id=1 event= +20:54:17.742 - httpx.dispatch.http2 - receive_event stream_id=0 event= +20:54:17.743 - httpx.dispatch.connection_pool - release_connection connection=HTTPConnection(origin=Origin(scheme='https' host='www.google.com' port=443)) +``` diff --git a/httpx/dispatch/connection.py b/httpx/dispatch/connection.py index 87c2a6489e..b843c2e53c 100644 --- a/httpx/dispatch/connection.py +++ b/httpx/dispatch/connection.py @@ -15,6 +15,7 @@ VerifyTypes, ) from ..models import AsyncRequest, AsyncResponse, Origin +from ..utils import get_logger from .base import AsyncDispatcher from .http2 import HTTP2Connection from .http11 import HTTP11Connection @@ -23,6 +24,9 @@ ReleaseCallback = typing.Callable[["HTTPConnection"], typing.Awaitable[None]] +logger = get_logger(__name__) + + class HTTPConnection(AsyncDispatcher): def __init__( self, @@ -79,8 +83,10 @@ async def connect( else: on_release = functools.partial(self.release_func, self) + logger.debug(f"start_connect host={host!r} port={port!r} timeout={timeout!r}") stream = await self.backend.connect(host, port, ssl_context, timeout) http_version = stream.get_http_version() + logger.debug(f"connected http_version={http_version!r}") if http_version == "HTTP/2": self.h2_connection = HTTP2Connection( @@ -102,6 +108,7 @@ async def get_ssl_context(self, ssl: SSLConfig) -> typing.Optional[ssl.SSLContex ) async def close(self) -> None: + logger.debug("close_connection") if self.h2_connection is not None: await self.h2_connection.close() elif self.h11_connection is not None: @@ -125,3 +132,7 @@ def is_connection_dropped(self) -> bool: else: assert self.h11_connection is not None return self.h11_connection.is_connection_dropped() + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + return f"{class_name}(origin={self.origin!r})" diff --git a/httpx/dispatch/connection_pool.py b/httpx/dispatch/connection_pool.py index eb990a9618..3699209197 100644 --- a/httpx/dispatch/connection_pool.py +++ b/httpx/dispatch/connection_pool.py @@ -12,12 +12,16 @@ VerifyTypes, ) from ..models import AsyncRequest, AsyncResponse, Origin +from ..utils import get_logger from .base import AsyncDispatcher from .connection import HTTPConnection CONNECTIONS_DICT = typing.Dict[Origin, typing.List[HTTPConnection]] +logger = get_logger(__name__) + + class ConnectionStore: """ We need to maintain collections of connections in a way that allows us to: @@ -122,6 +126,7 @@ async def send( return response async def acquire_connection(self, origin: Origin) -> HTTPConnection: + logger.debug(f"acquire_connection origin={origin!r}") connection = self.active_connections.pop_by_origin(origin, http2_only=True) if connection is None: connection = self.keepalive_connections.pop_by_origin(origin) @@ -141,12 +146,16 @@ async def acquire_connection(self, origin: Origin) -> HTTPConnection: backend=self.backend, release_func=self.release_connection, ) + logger.debug(f"new_connection connection={connection!r}") + else: + logger.debug(f"reuse_connection connection={connection!r}") self.active_connections.add(connection) return connection async def release_connection(self, connection: HTTPConnection) -> None: + logger.debug(f"release_connection connection={connection!r}") if connection.is_closed: self.active_connections.remove(connection) self.max_connections.release() diff --git a/httpx/dispatch/http11.py b/httpx/dispatch/http11.py index a54a8d4d8f..6bb178682c 100644 --- a/httpx/dispatch/http11.py +++ b/httpx/dispatch/http11.py @@ -5,6 +5,7 @@ from ..concurrency.base import BaseStream, ConcurrencyBackend, TimeoutFlag from ..config import TimeoutConfig, TimeoutTypes from ..models import AsyncRequest, AsyncResponse +from ..utils import get_logger H11Event = typing.Union[ h11.Request, @@ -22,6 +23,9 @@ OnReleaseCallback = typing.Callable[[], typing.Awaitable[None]] +logger = get_logger(__name__) + + class HTTP11Connection: READ_NUM_BYTES = 4096 @@ -73,6 +77,12 @@ async def _send_request( """ Send the request method, URL, and headers to the network. """ + logger.debug( + f"send_request method={request.method!r} " + f"target={request.url.full_path!r} " + f"headers={request.headers!r}" + ) + method = request.method.encode("ascii") target = request.url.full_path.encode("ascii") headers = request.headers.raw @@ -128,6 +138,12 @@ async def _receive_response( assert isinstance(event, h11.Response) break # pragma: no cover http_version = "HTTP/%s" % event.http_version.decode("latin-1", errors="ignore") + logger.debug( + f"receive_response " + f"status_code={event.status_code} " + f"headers={event.headers!r}" + ) + return http_version, event.status_code, event.headers async def _receive_response_data( @@ -150,6 +166,12 @@ async def _receive_event(self, timeout: TimeoutConfig = None) -> H11Event: """ while True: event = self.h11_state.next_event() + + if isinstance(event, h11.Data): + logger.debug(f"receive_event event=Data({len(event.data)})") + else: + logger.debug(f"receive_event event={event!r}") + if event is h11.NEED_DATA: try: data = await self.stream.read( @@ -164,6 +186,11 @@ async def _receive_event(self, timeout: TimeoutConfig = None) -> H11Event: return event async def response_closed(self) -> None: + logger.debug( + f"response_closed " + f"our_state={self.h11_state.our_state!r} " + f"their_state={self.h11_state.their_state}" + ) if ( self.h11_state.our_state is h11.DONE and self.h11_state.their_state is h11.DONE diff --git a/httpx/dispatch/http2.py b/httpx/dispatch/http2.py index f0ea158836..f1f6aade38 100644 --- a/httpx/dispatch/http2.py +++ b/httpx/dispatch/http2.py @@ -7,6 +7,9 @@ from ..concurrency.base import BaseStream, ConcurrencyBackend, TimeoutFlag from ..config import TimeoutConfig, TimeoutTypes from ..models import AsyncRequest, AsyncResponse +from ..utils import get_logger + +logger = get_logger(__name__) class HTTP2Connection: @@ -74,6 +77,9 @@ async def send_headers( (b":scheme", request.url.scheme.encode("ascii")), (b":path", request.url.full_path.encode("ascii")), ] + [(k, v) for k, v in request.headers.raw if k != b"host"] + + logger.debug(f"send_headers stream_id={stream_id} headers={headers!r}") + self.h2_state.send_headers(stream_id, headers) data_to_send = self.h2_state.data_to_send() await self.stream.write(data_to_send, timeout) @@ -100,11 +106,15 @@ async def send_data( chunk_size = min(len(data), flow_control) for idx in range(0, len(data), chunk_size): chunk = data[idx : idx + chunk_size] + + logger.debug(f"send_data stream_id={stream_id} data=Data({len(chunk)})") + self.h2_state.send_data(stream_id, chunk) data_to_send = self.h2_state.data_to_send() await self.stream.write(data_to_send, timeout) async def end_stream(self, stream_id: int, timeout: TimeoutConfig = None) -> None: + logger.debug(f"end_stream stream_id={stream_id}") self.h2_state.end_stream(stream_id) data_to_send = self.h2_state.data_to_send() await self.stream.write(data_to_send, timeout) @@ -151,7 +161,11 @@ async def receive_event( data = await self.stream.read(self.READ_NUM_BYTES, timeout, flag=flag) events = self.h2_state.receive_data(data) for event in events: - if getattr(event, "stream_id", 0): + event_stream_id = getattr(event, "stream_id", 0) + logger.debug( + f"receive_event stream_id={event_stream_id} event={event!r}" + ) + if event_stream_id: self.events[event.stream_id].append(event) data_to_send = self.h2_state.data_to_send() diff --git a/httpx/models.py b/httpx/models.py index 4016f89a9f..c511819cc5 100644 --- a/httpx/models.py +++ b/httpx/models.py @@ -243,6 +243,12 @@ def __eq__(self, other: typing.Any) -> bool: def __hash__(self) -> int: return hash((self.scheme, self.host, self.port)) + def __repr__(self) -> str: + class_name = self.__class__.__name__ + return ( + f"{class_name}(scheme={self.scheme!r} host={self.host!r} port={self.port})" + ) + class QueryParams(typing.Mapping[str, str]): """ diff --git a/httpx/utils.py b/httpx/utils.py index 6961326330..39f55d1fa0 100644 --- a/httpx/utils.py +++ b/httpx/utils.py @@ -1,7 +1,9 @@ import codecs +import logging import netrc import os import re +import sys import typing from pathlib import Path @@ -140,3 +142,31 @@ def parse_header_links(value: str) -> typing.List[typing.Dict[str, str]]: link[key.strip(replace_chars)] = value.strip(replace_chars) links.append(link) return links + + +_LOGGER_INITIALIZED = False + + +def get_logger(name: str) -> logging.Logger: + """Gets a `logging.Logger` instance and optionally + sets up debug logging if the user requests it via + the `HTTPX_DEBUG=1` environment variable. + """ + global _LOGGER_INITIALIZED + + # If the user wants debug logging on we set it up for them. + if not _LOGGER_INITIALIZED: + _LOGGER_INITIALIZED = True + if os.environ.get("HTTPX_DEBUG", "").lower() in ("1", "true"): + logger = logging.getLogger("httpx") + logger.setLevel(logging.DEBUG) + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter( + logging.Formatter( + fmt="%(asctime)s.%(msecs)03d - %(name)s - %(message)s", + datefmt="%H:%M:%S", + ) + ) + logger.addHandler(handler) + + return logging.getLogger(name) diff --git a/mkdocs.yml b/mkdocs.yml index 1d00aecfbb..b62bc50b36 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,6 +12,7 @@ nav: - Introduction: 'index.md' - QuickStart: 'quickstart.md' - Advanced Usage: 'advanced.md' + - Environment Variables: 'environment_variables.md' - Parallel Requests: 'parallel.md' - Async Client: 'async.md' - Requests Compatibility: 'compatibility.md' diff --git a/setup.cfg b/setup.cfg index 3077d46061..c8ed45fa63 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,7 @@ combine_as_imports = True force_grid_wrap = 0 include_trailing_comma = True known_first_party = httpx -known_third_party = brotli,certifi,chardet,cryptography,h11,h2,hstspreload,nox,pytest,rfc3986,setuptools,trustme,uvicorn +known_third_party = brotli,certifi,chardet,cryptography,h11,h2,hstspreload,nox,pytest,rfc3986,setuptools,tests,trustme,uvicorn line_length = 88 multi_line_output = 3 diff --git a/tests/dispatch/utils.py b/tests/dispatch/utils.py index b86552dc6b..2eb735a0ff 100644 --- a/tests/dispatch/utils.py +++ b/tests/dispatch/utils.py @@ -4,9 +4,9 @@ import h2.config import h2.connection import h2.events +from tests.concurrency import sleep from httpx import AsyncioBackend, BaseStream, Request, TimeoutConfig -from tests.concurrency import sleep class MockHTTP2Backend: From cdbfe86f973b8a082cc6b91da7ebde1039660c3c Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Mon, 26 Aug 2019 16:02:26 -0500 Subject: [PATCH 2/8] Update docs/environment_variables.md Co-Authored-By: Florimond Manca --- docs/environment_variables.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/environment_variables.md b/docs/environment_variables.md index bb55151461..d92721dec8 100644 --- a/docs/environment_variables.md +++ b/docs/environment_variables.md @@ -2,7 +2,7 @@ Environment Variables ===================== The HTTPX library can be configured via environment variables. -Here are a list of environment variables that HTTPX recognizes +Here is a list of environment variables that HTTPX recognizes and what function they serve: HTTPX_DEBUG From d610c3d6a22daf36f53a5f5963915481fac7c52e Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Mon, 26 Aug 2019 16:02:59 -0500 Subject: [PATCH 3/8] Update docs/environment_variables.md Co-Authored-By: Florimond Manca --- docs/environment_variables.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/environment_variables.md b/docs/environment_variables.md index d92721dec8..534e98647d 100644 --- a/docs/environment_variables.md +++ b/docs/environment_variables.md @@ -5,7 +5,7 @@ The HTTPX library can be configured via environment variables. Here is a list of environment variables that HTTPX recognizes and what function they serve: -HTTPX_DEBUG +`HTTPX_DEBUG` ----------- Valid values: `1`, `true` From 560e18c0542850b46d318dcba6beff15e3f29664 Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Mon, 26 Aug 2019 16:17:18 -0500 Subject: [PATCH 4/8] Update docs/environment_variables.md Co-Authored-By: Florimond Manca --- docs/environment_variables.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/environment_variables.md b/docs/environment_variables.md index 534e98647d..5e136d5bbe 100644 --- a/docs/environment_variables.md +++ b/docs/environment_variables.md @@ -24,7 +24,7 @@ Example: import httpx client = httpx.Client() -client.request("GET", "https://google.com") +client.get("https://google.com") ``` ```console From 69650da7272a3142a49a73716dc55ecb7c02a858 Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Mon, 26 Aug 2019 16:20:41 -0500 Subject: [PATCH 5/8] Incorperate suggested wording --- docs/environment_variables.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/environment_variables.md b/docs/environment_variables.md index 5e136d5bbe..0e65069b29 100644 --- a/docs/environment_variables.md +++ b/docs/environment_variables.md @@ -10,9 +10,8 @@ and what function they serve: Valid values: `1`, `true` -If this environment variable is set to `1` or `true` then -logging will be turned on by default to stderr about low-level -details of the HTTP request and responses being sent and received. +If this environment variable is set to `1` or `true` then low-level +details about the execution of HTTP requests will be logged to `stderr`. This can help you debug issues and see what's exactly being sent over the wire and to which location. From c08331483503cdf2de693f5394cbb7ceceda3f57 Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Mon, 26 Aug 2019 19:56:46 -0500 Subject: [PATCH 6/8] Suggestions --- docs/environment_variables.md | 2 +- httpx/utils.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/environment_variables.md b/docs/environment_variables.md index 0e65069b29..be1b5810b7 100644 --- a/docs/environment_variables.md +++ b/docs/environment_variables.md @@ -10,7 +10,7 @@ and what function they serve: Valid values: `1`, `true` -If this environment variable is set to `1` or `true` then low-level +If this environment variable is set to a valid value then low-level details about the execution of HTTP requests will be logged to `stderr`. This can help you debug issues and see what's exactly being sent diff --git a/httpx/utils.py b/httpx/utils.py index 39f55d1fa0..0357ea2bd2 100644 --- a/httpx/utils.py +++ b/httpx/utils.py @@ -154,7 +154,6 @@ def get_logger(name: str) -> logging.Logger: """ global _LOGGER_INITIALIZED - # If the user wants debug logging on we set it up for them. if not _LOGGER_INITIALIZED: _LOGGER_INITIALIZED = True if os.environ.get("HTTPX_DEBUG", "").lower() in ("1", "true"): From 3fdb9a874f611d83946ec75cde72f43f03c16166 Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Tue, 27 Aug 2019 07:10:09 -0500 Subject: [PATCH 7/8] Normalize logging output --- httpx/dispatch/http11.py | 12 ++++-------- httpx/dispatch/http2.py | 12 ++++++++++-- httpx/utils.py | 2 +- tests/dispatch/utils.py | 2 +- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/httpx/dispatch/http11.py b/httpx/dispatch/http11.py index 6bb178682c..7452b226ba 100644 --- a/httpx/dispatch/http11.py +++ b/httpx/dispatch/http11.py @@ -65,6 +65,7 @@ async def send( async def close(self) -> None: event = h11.ConnectionClosed() try: + logger.debug(f"send_event event={event!r}") self.h11_state.send(event) except h11.LocalProtocolError: # pragma: no cover # Premature client disconnect @@ -78,7 +79,7 @@ async def _send_request( Send the request method, URL, and headers to the network. """ logger.debug( - f"send_request method={request.method!r} " + f"send_headers method={request.method!r} " f"target={request.url.full_path!r} " f"headers={request.headers!r}" ) @@ -98,6 +99,7 @@ async def _send_request_data( try: # Send the request body. async for chunk in data: + logger.debug(f"send_data data=Data(<{len(chunk)} bytes>)") event = h11.Data(data=chunk) await self._send_event(event, timeout) @@ -138,12 +140,6 @@ async def _receive_response( assert isinstance(event, h11.Response) break # pragma: no cover http_version = "HTTP/%s" % event.http_version.decode("latin-1", errors="ignore") - logger.debug( - f"receive_response " - f"status_code={event.status_code} " - f"headers={event.headers!r}" - ) - return http_version, event.status_code, event.headers async def _receive_response_data( @@ -168,7 +164,7 @@ async def _receive_event(self, timeout: TimeoutConfig = None) -> H11Event: event = self.h11_state.next_event() if isinstance(event, h11.Data): - logger.debug(f"receive_event event=Data({len(event.data)})") + logger.debug(f"receive_event event=Data(<{len(event.data)} bytes>)") else: logger.debug(f"receive_event event={event!r}") diff --git a/httpx/dispatch/http2.py b/httpx/dispatch/http2.py index f1f6aade38..bb83d489b6 100644 --- a/httpx/dispatch/http2.py +++ b/httpx/dispatch/http2.py @@ -78,7 +78,13 @@ async def send_headers( (b":path", request.url.full_path.encode("ascii")), ] + [(k, v) for k, v in request.headers.raw if k != b"host"] - logger.debug(f"send_headers stream_id={stream_id} headers={headers!r}") + logger.debug( + f"send_headers " + f"stream_id={stream_id} " + f"method={request.method!r} " + f"target={request.url.full_path!r} " + f"headers={headers!r}" + ) self.h2_state.send_headers(stream_id, headers) data_to_send = self.h2_state.data_to_send() @@ -107,7 +113,9 @@ async def send_data( for idx in range(0, len(data), chunk_size): chunk = data[idx : idx + chunk_size] - logger.debug(f"send_data stream_id={stream_id} data=Data({len(chunk)})") + logger.debug( + f"send_data stream_id={stream_id} data=Data(<{len(chunk)} bytes>)" + ) self.h2_state.send_data(stream_id, chunk) data_to_send = self.h2_state.data_to_send() diff --git a/httpx/utils.py b/httpx/utils.py index 0357ea2bd2..0277e8118b 100644 --- a/httpx/utils.py +++ b/httpx/utils.py @@ -154,7 +154,7 @@ def get_logger(name: str) -> logging.Logger: """ global _LOGGER_INITIALIZED - if not _LOGGER_INITIALIZED: + if not _LOGGER_INITIALIZED: # pragma: no cover _LOGGER_INITIALIZED = True if os.environ.get("HTTPX_DEBUG", "").lower() in ("1", "true"): logger = logging.getLogger("httpx") diff --git a/tests/dispatch/utils.py b/tests/dispatch/utils.py index 2eb735a0ff..b86552dc6b 100644 --- a/tests/dispatch/utils.py +++ b/tests/dispatch/utils.py @@ -4,9 +4,9 @@ import h2.config import h2.connection import h2.events -from tests.concurrency import sleep from httpx import AsyncioBackend, BaseStream, Request, TimeoutConfig +from tests.concurrency import sleep class MockHTTP2Backend: From 2024938b4e5f4f27f7186b01b9cf8ab1b3b20353 Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Tue, 27 Aug 2019 11:25:26 -0500 Subject: [PATCH 8/8] Add unit tests for logging --- httpx/utils.py | 2 +- tests/test_utils.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/httpx/utils.py b/httpx/utils.py index 0277e8118b..0357ea2bd2 100644 --- a/httpx/utils.py +++ b/httpx/utils.py @@ -154,7 +154,7 @@ def get_logger(name: str) -> logging.Logger: """ global _LOGGER_INITIALIZED - if not _LOGGER_INITIALIZED: # pragma: no cover + if not _LOGGER_INITIALIZED: _LOGGER_INITIALIZED = True if os.environ.get("HTTPX_DEBUG", "").lower() in ("1", "true"): logger = logging.getLogger("httpx") diff --git a/tests/test_utils.py b/tests/test_utils.py index 2b2c7ab829..bb2abb4e04 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,10 @@ +import logging import os import pytest +import httpx +from httpx import utils from httpx.utils import get_netrc_login, guess_json_utf, parse_header_links @@ -87,3 +90,24 @@ def test_get_netrc_login(): ) def test_parse_header_links(value, expected): assert parse_header_links(value) == expected + + +@pytest.mark.asyncio +@pytest.mark.parametrize("httpx_debug", ["0", "1", "True", "False"]) +async def test_httpx_debug_enabled_stderr_logging(server, capsys, httpx_debug): + os.environ["HTTPX_DEBUG"] = httpx_debug + + # Force a reload on the logging handlers + utils._LOGGER_INITIALIZED = False + utils.get_logger("httpx") + + async with httpx.AsyncClient() as client: + await client.get("http://127.0.0.1:8000/") + + if httpx_debug in ("1", "True"): + assert "httpx.dispatch.connection_pool" in capsys.readouterr().err + else: + assert "httpx.dispatch.connection_pool" not in capsys.readouterr().err + + # Reset the logger so we don't have verbose output in all unit tests + logging.getLogger("httpx").handlers = []