`fetch()` always transparently decompresses the upstream body before
exposing it via `.text()` or the stream reader, so forwarding the
upstream `content-encoding` header (e.g. `gzip`) to the downstream
OpenAI/Anthropic-compatible client makes the client attempt to gunzip
plain text and fail with `ZlibError: incorrect header check`.
Similarly, `content-length` becomes stale once we transform the response
body (non-stream path repacks via `new Response()`; stream path
re-streams through our SSE heartbeat / shape transforms), and
`transfer-encoding` is owned by the Node/Next runtime, not us.
This patch adds `open-sse/utils/upstreamResponseHeaders.ts` exporting:
- `stripStaleEncodingHeaders(input: Headers)` — returns a new `Headers`
with content-encoding, content-length, transfer-encoding removed
(case-insensitive, does not mutate input).
- `filterUpstreamResponseHeaderEntries(entries, extraToStrip)` — same
semantics for the entries-array path used by the streaming response
builder, plus user-supplied additional names (case-insensitive).
- `STRIP_UPSTREAM_HEADER_NAMES` — the canonical set, exported for tests.
`open-sse/handlers/chatCore.ts` now uses these helpers in two places:
1. Non-stream path (~L3211): replaces `new Headers(rawResult.response.headers)`
with `stripStaleEncodingHeaders(...)` so the repacked Response below
doesn't claim a stale encoding/length.
2. Stream path (~L4371): replaces an inline IIFE+filter that only
removed `content-type` with `filterUpstreamResponseHeaderEntries(...,
["content-type"])` so the stale encoding/length are also stripped
before we set our own `text/event-stream` content-type.
Both call sites carry a comment explaining the underlying fetch
decompression behavior.
Tests
-----
New `tests/unit/upstream-response-headers-strip.test.ts` adds 10 cases
covering: lowercase strip, mixed-case strip, input non-mutation, empty
input, default-set filter, case-insensitive extraToStrip, empty
extraToStrip preservation, empty entries, mixed-case default names, and
canonical set identity.
Verified locally
----------------
- `npx eslint open-sse/utils/upstreamResponseHeaders.ts open-sse/handlers/chatCore.ts tests/unit/upstream-response-headers-strip.test.ts` — clean.
- `npm run typecheck:core` — clean.
- `node --import tsx/esm --test tests/unit/upstream-response-headers-strip.test.ts tests/unit/upstream-headers-sanitize.test.ts tests/unit/chatcore-sanitization.test.ts tests/unit/chat-route-edge-cases.test.ts tests/unit/chatcore-translation-paths.test.ts tests/unit/chatcore-compression-integration.test.ts` — 95/95 pass.
- Full `npm run test:unit` / `test:coverage` deferred to upstream CI
(122 test files, exceeds local timeout window).