Skip to content

fix(token-spy): rebuild headers for decoded responses - #2971

Open
tang-vu wants to merge 1 commit into
Osmantic:mainfrom
tang-vu:fix/token-spy-decoded-response-headers
Open

fix(token-spy): rebuild headers for decoded responses#2971
tang-vu wants to merge 1 commit into
Osmantic:mainfrom
tang-vu:fix/token-spy-decoded-response-headers

Conversation

@tang-vu

@tang-vu tang-vu commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Why this matters

Token Spy returns resp.content, which HTTPX automatically decompresses, but copied the upstream headers unchanged. A gzip response therefore reached clients as decoded bytes still labeled Content-Encoding: gzip and with the compressed Content-Length; real HTTPX clients fail with DecodingError, and other clients can truncate or double-decode the payload.

Root cause: representation and hop-by-hop headers were reused after the response representation had changed. HTTPX documents that Response.content automatically decodes gzip/deflate while raw streaming is required to preserve wire encoding: https://www.python-httpx.org/quickstart/#binary-response-content

Behavioral invariant: buffered proxy responses describe the body actually sent downstream, while safe end-to-end metadata remains intact.

Overlap check

Searched open and closed upstream PRs for Token Spy content-encoding/content-length/decoded-response/header passthrough terms and inspected current PRs touching ods/extensions/services/token-spy/main.py. Existing changes cover streaming status/error logging, SSE cursors, pricing, persistence, and request auth; none rebuild response headers after HTTPX decoding. This scope is independent.

What changed

  • Added one response-header filter for decoded, buffered proxy bodies.
  • Removed stale Content-Encoding/Content-Length and standard hop-by-hop fields.
  • Applied the contract to Anthropic non-streaming, OpenAI-compatible non-streaming, and catch-all passthrough responses.
  • Preserved end-to-end headers such as content type and upstream request IDs; Starlette now emits the correct downstream content length.

Validation

  • Red before fix: all three public proxy paths failed at the client boundary; HTTPX attempted a second gzip decode and raised DecodingError.
  • pytest tests/test_decoded_response_headers.py -q — 3 passed.
  • pytest tests -q — 23 passed, 1 skipped.
  • python -m py_compile main.py tests/test_decoded_response_headers.py — passed.
  • git diff --check — passed.

Tradeoffs and rollback

This keeps buffered proxy behavior; it does not convert large non-streaming responses to raw streaming. Headers tied to a specific hop or encoded wire body are intentionally not forwarded. Rollback is one commit with no persisted-state impact.

Batch compatibility

Validated as an independent ten-PR batch from upstream main 6ff9b4fc5190099705043acaab7e9b6ad9c8b8f1. The final PR heads merged without conflicts in this order: #2964 -> #2965 -> #2967 -> #2969 -> #2970 -> #2971 -> #2972 -> #2973 -> #2974 -> #2975. The resulting local synthetic merge head is 4ae60eadad9696a9accb735aad71af87d2be802d.

Combined validation on that exact tree:

  • Dashboard API boundary suites: 323 passed, 4 skipped.
  • Token Spy suite: 36 passed, 1 skipped.
  • Privacy Shield suite: 55 passed.
  • APE suite: 47 passed.
  • Python compile checks and git diff --check: passed.

The scopes are behaviorally independent. The stated order is the tested rollback/merge sequence for shared-file changes; each PR remains individually useful and revertible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant