From 2a34a822f2a39da57fbd07575752888f5515f51a Mon Sep 17 00:00:00 2001 From: Vinay Gupta <58447456+aivinay@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:37:47 -0500 Subject: [PATCH] fix(proxy): preserve Responses passthrough bytes (#1598) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Fixes the Python `/v1/responses` forwarding path for encoded Codex Desktop requests. When Headroom receives a compressed Responses request, the request body is decoded before JSON parsing. The handler then forwarded a rewritten JSON body while preserving the inbound `Content-Encoding` header, so upstream could receive plain JSON bytes that were still labeled as `zstd`/`gzip`. This change keeps the decoded original bytes for true passthrough requests, strips stale entity headers, and marks Responses body mutations so memory/compression paths still use canonical serialization. Closes #1542 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Switched `/v1/responses` parsing to keep the decoded original request bytes. - Stripped stale `content-encoding` and `transfer-encoding` headers before forwarding decoded JSON bodies. - Wired Responses streaming and non-streaming forwarding through the existing byte-faithful passthrough controls. - Marked Responses memory and compression body mutations so mutated requests continue to serialize canonically. - Added regression tests for gzip and zstd encoded Responses passthrough bodies. ## Testing - [x] Unit tests pass (`pytest`) — GitHub CI test shards passed - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) — GitHub CI ran `mypy headroom --ignore-missing-imports` - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ /tmp/headroom-1542-testenv/bin/python -m ruff check . All checks passed! $ /tmp/headroom-1542-testenv/bin/python -m ruff format --check . 1014 files already formatted! $ HEADROOM_REQUIRE_RUST_CORE=false PYTHONPATH=/Users/vinaygupta/Desktop/git/headroom-fix-1542-zstd-passthrough /tmp/headroom-1542-testenv/bin/python - <<'PY' # Injected an in-memory headroom._core import stub for this local checkout, # then ran pytest.main(["tests/test_openai_codex_routing.py", "-q"]) PY 19 passed in 0.55s $ HEADROOM_REQUIRE_RUST_CORE=false PYTHONPATH=/Users/vinaygupta/Desktop/git/headroom-fix-1542-zstd-passthrough /tmp/headroom-1542-testenv/bin/python - <<'PY' # Injected an in-memory headroom._core import stub for this local checkout, # then ran pytest.main(["tests/test_proxy_byte_faithful_forwarding.py", "-q"]) PY 35 passed, 1 warning in 1.33s $ HEADROOM_REQUIRE_RUST_CORE=false PYTHONPATH=/Users/vinaygupta/Desktop/git/headroom-fix-1542-zstd-passthrough /tmp/headroom-1542-testenv/bin/python - <<'PY' # Injected the same in-memory headroom._core import stub, # then ran pytest.main(["tests/test_proxy_compression_headers.py", "-q"]) PY 10 passed in 0.05s GitHub CI on `66507d98d41902730f0fb2b517cbbf493f47f2b9`: - `lint`: SUCCESS, including `ruff check .`, `ruff format --check .`, and `mypy headroom --ignore-missing-imports` (`Success: no issues found in 404 source files`). - `test (1)` through `test (4)`: SUCCESS. - `test-agno`, `test-extras`, `test-dashboard-ui`, `docker-init-e2e`, `docker-wrap-e2e`, and `docker-native-e2e`: SUCCESS. - PR Governance and Security checks: SUCCESS. ``` ## Real Behavior Proof - Environment: macOS local checkout, Python 3.13 throwaway test env. - Exact command / steps: sent encoded `/v1/responses` test requests through `TestClient` with a fake upstream transport capturing outbound bytes and headers. - Observed result: upstream received decoded JSON bytes, no stale `content-encoding`, recomputed `content-length`, and logs reported `body_mutated=false source=passthrough`. - Not tested: full `pytest` and `mypy headroom` were not run locally. A normal `uv run pytest ...` attempt was blocked by the local native build error in `esaxx-rs` (`fatal error: 'cstdint' file not found`). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A ## Additional Notes README/docs and CHANGELOG updates are not applicable for this narrow proxy bug fix. GitHub CI is green on the rebased branch. Full local pytest and local mypy were not run because the local native extension build was blocked, so the full-suite signal comes from GitHub Actions. --- headroom/proxy/handlers/openai.py | 31 +++- tests/test_openai_codex_routing.py | 2 +- tests/test_proxy_byte_faithful_forwarding.py | 155 +++++++++++++++++++ 3 files changed, 184 insertions(+), 4 deletions(-) diff --git a/headroom/proxy/handlers/openai.py b/headroom/proxy/handlers/openai.py index 0857b4167..f8d7e303b 100644 --- a/headroom/proxy/handlers/openai.py +++ b/headroom/proxy/handlers/openai.py @@ -2922,7 +2922,8 @@ class OpenAIHandlerMixin: from headroom.proxy.helpers import ( MAX_REQUEST_BODY_SIZE, - _read_request_json, + BodyMutationTracker, + read_request_json_with_bytes, ) from headroom.tokenizers import get_tokenizer from headroom.utils import extract_user_query @@ -2954,7 +2955,7 @@ class OpenAIHandlerMixin: # Parse request try: - body = await _read_request_json(request) + body, original_body_bytes = await read_request_json_with_bytes(request) except (json.JSONDecodeError, ValueError) as e: return JSONResponse( status_code=400, @@ -2969,6 +2970,7 @@ class OpenAIHandlerMixin: model = body.get("model", "unknown") stream = body.get("stream", False) + body_mutation_tracker = BodyMutationTracker() _bypass = self._headroom_bypass_enabled(request.headers) if _bypass: logger.info( @@ -3009,6 +3011,10 @@ class OpenAIHandlerMixin: headers = dict(request.headers.items()) headers.pop("host", None) headers.pop("content-length", None) + # The parsed request body has already been content-decoded. Remove + # entity headers that described the client-to-proxy wire body. + headers.pop("content-encoding", None) + headers.pop("transfer-encoding", None) # Strip accept-encoding so httpx negotiates its own encoding. # Cloudflare Workers forward "br, zstd" which OpenAI may honor; # if httpx lacks brotli support the response body is undecipherable → 502. @@ -3195,6 +3201,7 @@ class OpenAIHandlerMixin: if current_input else memory_context ) + body_mutation_tracker.mark_mutated("responses_memory_context") log_memory_injection( request_id=request_id, session_id=None, @@ -3208,6 +3215,7 @@ class OpenAIHandlerMixin: ) if bytes_appended > 0: body["input"] = new_input + body_mutation_tracker.mark_mutated("responses_memory_context") log_memory_injection( request_id=request_id, session_id=None, @@ -3272,12 +3280,14 @@ class OpenAIHandlerMixin: ) if mem_tools_injected: body["tools"] = resp_tools + body_mutation_tracker.mark_mutated("responses_memory_tools") logger.info(f"[{request_id}] Memory: Injected memory tools (openai/responses)") if _ensure_responses_store_for_memory_tools( body, memory_tools_injected=True, ): + body_mutation_tracker.mark_mutated("responses_memory_store") logger.info( f"[{request_id}] Memory: forced store=true for Responses memory tool continuation" ) @@ -3343,6 +3353,7 @@ class OpenAIHandlerMixin: ) attempted_input_tokens = int(_attempted_tokens) if _modified: + body_mutation_tracker.mark_mutated("responses_compression") tokens_saved = int(_tokens_saved) optimized_tokens = max(0, original_tokens - tokens_saved) transforms_applied = [*_transforms, *list(transforms_applied)] @@ -3468,11 +3479,25 @@ class OpenAIHandlerMixin: optimization_latency, memory_user_id=memory_user_id, memory_request_ctx=memory_request_ctx, + original_body_bytes=original_body_bytes, + body_mutated=body_mutation_tracker.mutated, + mutation_reasons=body_mutation_tracker.reasons, waste_signals=waste_signals_dict, ) else: headers = await apply_copilot_api_auth(headers, url=url) - response = await self._retry_request("POST", url, headers, body) + response = await self._retry_request( + "POST", + url, + headers, + body, + original_body_bytes=original_body_bytes, + body_mutated=body_mutation_tracker.mutated, + mutation_reasons=body_mutation_tracker.reasons, + request_id=request_id, + forwarder_name="openai_responses", + path_for_log=url, + ) _response_body_for_debug: Any = None _response_raw_for_debug: str | None = None try: diff --git a/tests/test_openai_codex_routing.py b/tests/test_openai_codex_routing.py index fb54222fd..d8be9e6e3 100644 --- a/tests/test_openai_codex_routing.py +++ b/tests/test_openai_codex_routing.py @@ -192,7 +192,7 @@ class _DummyOpenAIHandler(OpenAIHandlerMixin): def _extract_tags(self, headers: dict[str, str]) -> dict[str, str]: return {} - async def _retry_request(self, method: str, url: str, headers: dict, body: dict): + async def _retry_request(self, method: str, url: str, headers: dict, body: dict, **kwargs): self.captured_request = (method, url, headers, body) return _ResponseStub() diff --git a/tests/test_proxy_byte_faithful_forwarding.py b/tests/test_proxy_byte_faithful_forwarding.py index 027956c5b..b720720f8 100644 --- a/tests/test_proxy_byte_faithful_forwarding.py +++ b/tests/test_proxy_byte_faithful_forwarding.py @@ -18,8 +18,10 @@ rollback (operator opt-in, not a fallback). from __future__ import annotations +import gzip import hashlib import json +import logging from types import SimpleNamespace from unittest.mock import AsyncMock @@ -340,6 +342,92 @@ def _make_no_optimize_app() -> tuple[TestClient, _CapturingTransport]: return _make_anthropic_app(optimize=False) +def _openai_responses_body_bytes(*, stream: bool) -> bytes: + payload = { + "model": "gpt-5.5", + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "hello 🔥 with spaces preserved", + } + ], + } + ], + "stream": stream, + } + return json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8") + + +def _openai_responses_codex_headers(content_encoding: str) -> dict[str, str]: + return { + "authorization": "Bearer test-token", + "chatgpt-account-id": "acct_test", + "originator": "Codex Desktop", + "content-type": "application/json", + "content-encoding": content_encoding, + "accept": "text/event-stream", + } + + +def _start_proxy_log_capture() -> tuple[ + logging.Logger, + logging.Handler, + int, + list[logging.LogRecord], +]: + proxy_logger = logging.getLogger("headroom.proxy") + records: list[logging.LogRecord] = [] + + class _ListHandler(logging.Handler): + def emit(self, record: logging.LogRecord) -> None: + records.append(record) + + handler = _ListHandler(level=logging.INFO) + prev_level = proxy_logger.level + proxy_logger.addHandler(handler) + proxy_logger.setLevel(logging.INFO) + return proxy_logger, handler, prev_level, records + + +def _stop_proxy_log_capture( + proxy_logger: logging.Logger, + handler: logging.Handler, + prev_level: int, +) -> None: + proxy_logger.removeHandler(handler) + proxy_logger.setLevel(prev_level) + + +def _assert_openai_responses_encoded_passthrough( + transport: _CapturingTransport, + decoded_body: bytes, +) -> None: + assert transport.captured_body == decoded_body + assert transport.captured_headers is not None + captured_headers = {key.lower(): value for key, value in transport.captured_headers.items()} + assert "content-encoding" not in captured_headers + assert captured_headers.get("content-length") == str(len(decoded_body)) + + +def _assert_outbound_passthrough_log( + records: list[logging.LogRecord], + *, + forwarder: str, +) -> None: + messages = [record.getMessage() for record in records] + assert any( + "event=outbound_request" in message + and f"forwarder={forwarder}" in message + and "body_mutated=false" in message + and "source=passthrough" in message + for message in messages + ), messages + + def test_passthrough_no_mutation_byte_equal_sha256() -> None: """No transform → upstream SHA-256 equals client-sent SHA-256.""" client, transport = _make_no_optimize_app() @@ -904,6 +992,73 @@ def test_streaming_forwarder_byte_faithful() -> None: ) +def test_openai_responses_gzip_nonstream_passthrough_strips_content_encoding() -> None: + client, transport = _make_no_optimize_app() + decoded_body = _openai_responses_body_bytes(stream=False) + encoded_body = gzip.compress(decoded_body) + proxy_logger, handler, prev_level, records = _start_proxy_log_capture() + + try: + response = client.post( + "/v1/responses", + headers=_openai_responses_codex_headers("gzip"), + content=encoded_body, + ) + finally: + _stop_proxy_log_capture(proxy_logger, handler, prev_level) + + assert response.status_code == 200, response.text + _assert_openai_responses_encoded_passthrough(transport, decoded_body) + _assert_outbound_passthrough_log(records, forwarder="openai_responses") + + +def test_openai_responses_gzip_stream_passthrough_strips_content_encoding() -> None: + client, transport = _make_no_optimize_app() + decoded_body = _openai_responses_body_bytes(stream=True) + encoded_body = gzip.compress(decoded_body) + proxy_logger, handler, prev_level, records = _start_proxy_log_capture() + + try: + with client.stream( + "POST", + "/v1/responses", + headers=_openai_responses_codex_headers("gzip"), + content=encoded_body, + ) as response: + assert response.status_code == 200 + for _ in response.iter_bytes(): + pass + finally: + _stop_proxy_log_capture(proxy_logger, handler, prev_level) + + _assert_openai_responses_encoded_passthrough(transport, decoded_body) + _assert_outbound_passthrough_log(records, forwarder="streaming") + + +def test_openai_responses_codex_desktop_zstd_stream_passthrough_strips_content_encoding() -> None: + zstandard = pytest.importorskip("zstandard") + client, transport = _make_no_optimize_app() + decoded_body = _openai_responses_body_bytes(stream=True) + encoded_body = zstandard.ZstdCompressor().compress(decoded_body) + proxy_logger, handler, prev_level, records = _start_proxy_log_capture() + + try: + with client.stream( + "POST", + "/v1/responses", + headers=_openai_responses_codex_headers("zstd"), + content=encoded_body, + ) as response: + assert response.status_code == 200 + for _ in response.iter_bytes(): + pass + finally: + _stop_proxy_log_capture(proxy_logger, handler, prev_level) + + _assert_openai_responses_encoded_passthrough(transport, decoded_body) + _assert_outbound_passthrough_log(records, forwarder="streaming") + + # --------------------------------------------------------------------------- # Batch forwarder byte-faithfulness (passthrough variant) # ---------------------------------------------------------------------------