From 87b52d1ed6d3c19875f636c62fb2deca26156477 Mon Sep 17 00:00:00 2001 From: officer Date: Wed, 19 Aug 2026 09:18:07 +0000 Subject: [PATCH] drop content-encoding on every response, not just rewritten ones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bun's fetch sets its own Accept-Encoding and decompresses transparently, so upstream.body is plain while upstream.headers still says gzip. Passing both on hands the next hop a body that contradicts its own header, and it dies with ZlibError. Fixed for the HTML branch and missed on the pass-through, so the page rendered while every stylesheet, script and image beside it answered 502 — which reads as a broken asset pipeline rather than one header on one branch. --- sidecar/index.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/sidecar/index.ts b/sidecar/index.ts index f257a5f..4fac38c 100644 --- a/sidecar/index.ts +++ b/sidecar/index.ts @@ -284,6 +284,17 @@ const server = Bun.serve({ out.delete('x-frame-options'); out.delete('content-security-policy'); for (const h of ['connection', 'keep-alive', 'transfer-encoding']) out.delete(h); + // CONTENT-ENCODING ALWAYS, not just when rewriting. + // + // Bun's `fetch` sets its own Accept-Encoding and decompresses the response transparently, so + // `upstream.body` is plain while `upstream.headers` still says gzip. Passing both on hands the + // next hop a body that does not match its own header, and it dies with ZlibError. + // + // This was fixed for the HTML branch below and missed here, so the PAGE rendered while every + // stylesheet, script and image beside it answered 502 — which looks like a broken asset pipeline + // rather than one header. + out.delete('content-encoding'); + out.delete('content-length'); // Where the browser thinks this is. Passed by the plugin's router rather than hardcoded — the // sidecar has no business knowing how the platform routes to it, and `mountPrefix` is the only @@ -307,12 +318,8 @@ const server = Bun.serve({ // `appSubUrl`, patched below by the same pass, is what makes Gitea's own scripts correct. if ((out.get('content-type') ?? '').includes('text/html')) { const body = rewriteHtml(await upstream.text(), base, origins); - // BOTH of these, and the second is not optional. `.text()` has already decompressed, so the - // body being returned is plain — while `out` still carries Gitea's `content-encoding: gzip`. - // The next hop believes that header, tries to gunzip plain text, and fails with ZlibError: - // a 502 that names nothing and looks exactly like the sidecar being unreachable. - out.delete('content-length'); // the body just changed length - out.delete('content-encoding'); // and is no longer compressed + // `content-encoding` and `content-length` are already gone, dropped for every response + // above — the body changing length here is one more reason they have to be. return new Response(body, { status: upstream.status, headers: out }); } }