From 58d7713581f18fc2460900a21029e422796729e1 Mon Sep 17 00:00:00 2001 From: jwcrystal <121911854+jwcrystal@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:45:19 +0800 Subject: [PATCH] fix(server): strip compression headers in generic OpenCode proxy (#795) Co-authored-by: Bohdan Triapitsyn --- packages/web/server/proxy-headers.js | 61 +++++++++++++++++++++++ packages/web/server/proxy-headers.test.js | 53 ++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 packages/web/server/proxy-headers.js create mode 100644 packages/web/server/proxy-headers.test.js diff --git a/packages/web/server/proxy-headers.js b/packages/web/server/proxy-headers.js new file mode 100644 index 00000000..e82cef12 --- /dev/null +++ b/packages/web/server/proxy-headers.js @@ -0,0 +1,61 @@ +const filteredRequestHeaders = new Set([ + 'host', + 'connection', + 'content-length', + 'transfer-encoding', + 'keep-alive', + 'te', + 'trailer', + 'upgrade', + 'accept-encoding', +]); + +const filteredResponseHeaders = new Set([ + 'connection', + 'content-length', + 'transfer-encoding', + 'keep-alive', + 'te', + 'trailer', + 'upgrade', + 'www-authenticate', + 'content-encoding', +]); + +export const collectForwardProxyHeaders = (requestHeaders, authHeaders = {}) => { + const headers = {}; + + for (const [key, value] of Object.entries(requestHeaders || {})) { + if (!value) continue; + const normalizedKey = key.toLowerCase(); + if (filteredRequestHeaders.has(normalizedKey)) continue; + headers[normalizedKey] = Array.isArray(value) ? value.join(', ') : String(value); + } + + if (authHeaders.Authorization) { + headers.Authorization = authHeaders.Authorization; + } + + return headers; +}; + +export const shouldForwardProxyResponseHeader = (key) => { + if (typeof key !== 'string' || key.trim().length === 0) { + return false; + } + + return !filteredResponseHeaders.has(key.toLowerCase()); +}; + +export const applyForwardProxyResponseHeaders = (responseHeaders, response) => { + if (!responseHeaders || typeof response?.setHeader !== 'function') { + return; + } + + for (const [key, value] of responseHeaders.entries()) { + if (!shouldForwardProxyResponseHeader(key)) { + continue; + } + response.setHeader(key, value); + } +}; diff --git a/packages/web/server/proxy-headers.test.js b/packages/web/server/proxy-headers.test.js new file mode 100644 index 00000000..b0e85353 --- /dev/null +++ b/packages/web/server/proxy-headers.test.js @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'bun:test'; + +import { + applyForwardProxyResponseHeaders, + collectForwardProxyHeaders, + shouldForwardProxyResponseHeader, +} from './proxy-headers.js'; + +describe('OpenCode proxy header handling', () => { + it('drops accept-encoding from forwarded request headers', () => { + const headers = collectForwardProxyHeaders({ + accept: 'application/json', + 'accept-encoding': 'gzip, deflate, br', + connection: 'keep-alive', + }); + + expect(headers.accept).toBe('application/json'); + expect(headers['accept-encoding']).toBeUndefined(); + }); + + it('drops content-encoding from forwarded response headers', () => { + expect(shouldForwardProxyResponseHeader('content-encoding')).toBe(false); + expect(shouldForwardProxyResponseHeader('Content-Encoding')).toBe(false); + }); + + it('still keeps ordinary response headers', () => { + expect(shouldForwardProxyResponseHeader('content-type')).toBe(true); + expect(shouldForwardProxyResponseHeader('etag')).toBe(true); + }); + + it('applies upstream response headers to express response without content-encoding', () => { + const applied = []; + const response = { + setHeader(key, value) { + applied.push([key, value]); + }, + }; + + applyForwardProxyResponseHeaders( + new Headers({ + 'content-type': 'application/json', + etag: 'W/"abc"', + 'content-encoding': 'gzip', + }), + response, + ); + + expect(applied).toEqual([ + ['content-type', 'application/json'], + ['etag', 'W/"abc"'], + ]); + }); +});