From 1c08e7362c2f0d1c75f8bc806d01b1db646458d9 Mon Sep 17 00:00:00 2001 From: febux Date: Sun, 6 Sep 2026 20:41:35 +0300 Subject: [PATCH] Fix stream not read error for files whose recieved from bot. --- src/contract_check/core/http_logging.py | 9 ++++++- tests/unit/test_http_logging.py | 34 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/contract_check/core/http_logging.py b/src/contract_check/core/http_logging.py index 9a8511a..1a6a2b7 100644 --- a/src/contract_check/core/http_logging.py +++ b/src/contract_check/core/http_logging.py @@ -147,13 +147,20 @@ def http_log_event_hooks( request.extensions[started_key] = time.perf_counter() if not is_debug_enabled(): return + try: + body: bytes | str | None = request.content + except httpx.RequestNotRead: + # Streaming bodies (e.g. multipart file uploads) are never loaded + # into memory, so there is nothing to inspect without consuming + # the stream the actual send needs. Summarize instead of crashing. + body = "" log.debug( "http_request", service=service, method=request.method, url=str(request.url), headers=redact_headers(request.headers), - body=safe_body(request.content, request.headers.get("content-type"), max_payload_chars), + body=safe_body(body, request.headers.get("content-type"), max_payload_chars), ) async def _log_response(response: httpx.Response) -> None: diff --git a/tests/unit/test_http_logging.py b/tests/unit/test_http_logging.py index 57476b7..90c17f7 100644 --- a/tests/unit/test_http_logging.py +++ b/tests/unit/test_http_logging.py @@ -145,3 +145,37 @@ async def test_event_hooks_silent_without_debug(monkeypatch: pytest.MonkeyPatch) await client.get("https://api.test/v1/things") assert logs == [] + + +async def test_event_hooks_handle_streaming_multipart_body( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Multipart uploads stream their body; the request hook must not crash.""" + monkeypatch.setattr(hhttp, "is_debug_enabled", lambda: True) + _config = structlog.get_config() + monkeypatch.setattr( + hhttp, + "get_logger", + lambda name: structlog.get_logger(name), + ) + structlog.configure( + processors=_config["processors"], + wrapper_class=structlog.stdlib.BoundLogger, + logger_factory=_config["logger_factory"], + cache_logger_on_first_use=False, + ) + with capture_logs() as logs: + transport = httpx.MockTransport(lambda request: httpx.Response(202, json={"ok": True})) + async with httpx.AsyncClient( + transport=transport, + event_hooks=http_log_event_hooks(service="test-svc"), + ) as client: + response = await client.post( + "https://api.test/v1/documents", + files={"file": ("contract.pdf", b"%PDF-1.7 fake", "application/pdf")}, + ) + + assert response.status_code == 202 + request_entry = next(entry for entry in logs if entry.get("event") == "http_request") + assert request_entry["method"] == "POST" + assert request_entry["body"] == ""