fix(exit_node): strip Content-Encoding + Content-Length on response (#964)

@mehdimalekidev reported `Content Encoding Error` when ChatGPT was routed
through Apps Script + exit-node. Root cause:

1. Browser → Apps Script with `Accept-Encoding: gzip, br` (default).
2. Apps Script forwards header to exit-node.
3. exit-node calls `fetch(destination)` which **auto-decompresses** the
   response body (Deno / Bun / Node `fetch()` does this by default).
4. `resp.arrayBuffer()` returns **plain decompressed bytes** but
   `resp.headers` still has `Content-Encoding: gzip` from the destination.
5. exit-node forwards both — Apps Script + Rust client pass them through —
   browser sees `Content-Encoding: gzip` on plain bytes → "Content Encoding
   Error: invalid or unsupported form of compression".

Fix: strip `Content-Encoding` and `Content-Length` from the response
headers before returning to the relay. The Apps Script + Rust transport
layer reframes the wire body anyway, so neither header is meaningful to
forward end-to-end.

Affects ChatGPT (gzip), Claude (br), Reddit (gzip), and any other
compressed exit-node-routed destination — the fix makes them all work.

No new test (this fixes a regression that would only show in a real
fetch path; mocking auto-decompression behavior is fragile). Manual
verification: tested ChatGPT through exit-node, response renders normally
in Firefox.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
therealaleph
2026-05-09 21:27:58 +03:00
parent 072a917331
commit c437598169
+11
View File
@@ -146,9 +146,20 @@ export default async function (req: Request): Promise<Response> {
redirect: "manual", redirect: "manual",
}); });
// `fetch()` (Deno / Bun / Node) auto-decompresses gzip / br / deflate
// responses, so `resp.arrayBuffer()` returns plain bytes — but the
// destination's `Content-Encoding` header is still on `resp.headers`.
// Forwarding it would tell the client browser "this body is gzipped"
// when it isn't, producing `Content Encoding Error` (#964). Same goes
// for `Content-Length` — the post-decompression byte count is
// different from what the destination announced. Strip both. The
// Apps Script + Rust transport layer below us re-frames the wire body
// anyway, so neither header is meaningful to forward.
const data = new Uint8Array(await resp.arrayBuffer()); const data = new Uint8Array(await resp.arrayBuffer());
const respHeaders: Record<string, string> = {}; const respHeaders: Record<string, string> = {};
resp.headers.forEach((value, key) => { resp.headers.forEach((value, key) => {
const lower = key.toLowerCase();
if (lower === "content-encoding" || lower === "content-length") return;
respHeaders[key] = value; respHeaders[key] = value;
}); });