From 13cf4ffa19d4a4c46272638529a222b86ef76dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Gonz=C3=A1lez?= Date: Sun, 19 Oct 2025 11:57:49 -0600 Subject: [PATCH 1/2] fix(start-server-core): use Headers.entries() to iterate headers correctly The previous code used Object.entries(headers) which doesn't work on Headers objects from the Web API, resulting in no headers being set at all. Changed to use headers.entries() which correctly iterates through the Headers object and properly handles multiple headers with the same name. - Fixed setResponseHeaders to use headers.entries() instead of Object.entries() - Added logic to use .set() for first occurrence, .append() for duplicates - Added test for multiple headers with the same name --- .../start-server-core/src/request-response.ts | 12 +- .../tests/request-response.test.ts | 109 ++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 packages/start-server-core/tests/request-response.test.ts diff --git a/packages/start-server-core/src/request-response.ts b/packages/start-server-core/src/request-response.ts index cf0e426db9c..4c0ab588fbb 100644 --- a/packages/start-server-core/src/request-response.ts +++ b/packages/start-server-core/src/request-response.ts @@ -136,8 +136,16 @@ export function setResponseHeaders( headers: TypedHeaders, ): void { const event = getH3Event() - for (const [name, value] of Object.entries(headers)) { - event.res.headers.set(name, value) + const addedHeaderNames: Record = {} + for (const [name, value] of headers.entries()) { + const found = addedHeaderNames[name] ?? false + if (!found) { + addedHeaderNames[name] = true + } + // If header already existed in h3 event headers, it will be replaced. + // However, headers object in this invocation might have multiple instances of the same header name (.append() was used), let's allow the duplicates. + const method = found ? 'append' : 'set' + event.res.headers[method](name, value) } } diff --git a/packages/start-server-core/tests/request-response.test.ts b/packages/start-server-core/tests/request-response.test.ts new file mode 100644 index 00000000000..16edf422de1 --- /dev/null +++ b/packages/start-server-core/tests/request-response.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it } from 'vitest' +import { + getResponseHeader, + getResponseHeaders, + requestHandler, + setResponseHeaders, +} from '../src/request-response' + +describe('setResponseHeaders', () => { + it('should set a single header via Headers object', async () => { + const headers = new Headers() + headers.set('X-Custom-Header', 'test-value') + + const handler = requestHandler(() => { + setResponseHeaders(headers) + const responseHeaders = getResponseHeaders() + expect(responseHeaders.get('X-Custom-Header')).toBe('test-value') + return new Response('OK') + }) + + const request = new Request('http://localhost:3000/test') + await handler(request, {}) + }) + + it('should set multiple headers via Headers object', async () => { + const headers = new Headers() + headers.set('X-Custom-Header', 'test-value') + headers.set('X-Another-Header', 'another-value') + headers.set('Content-Type', 'application/json') + + const handler = requestHandler(() => { + setResponseHeaders(headers) + const responseHeaders = getResponseHeaders() + expect(responseHeaders.get('X-Custom-Header')).toBe('test-value') + expect(responseHeaders.get('X-Another-Header')).toBe('another-value') + expect(responseHeaders.get('Content-Type')).toBe('application/json') + return new Response('OK') + }) + + const request = new Request('http://localhost:3000/test') + await handler(request, {}) + }) + + it('should handle empty Headers object', async () => { + const handler = requestHandler(() => { + const headers = new Headers() + setResponseHeaders(headers) + const responseHeaders = getResponseHeaders() + expect(responseHeaders).toBeDefined() + expect(Array.from(responseHeaders.entries()).length).toEqual(0) + return new Response('OK') + }) + + const request = new Request('http://localhost:3000/test') + await handler(request, {}) + }) + + it('should replace existing headers with the same name', async () => { + const headers = new Headers() + headers.set('X-Custom-Header', 'old-value') + + const handler = requestHandler(() => { + setResponseHeaders( + new Headers({ + 'X-Custom-Header': 'old-value', + }), + ) + expect(getResponseHeader('X-Custom-Header')).toEqual('old-value') + setResponseHeaders( + new Headers({ + 'X-Custom-Header': 'new-value', + }), + ) + expect(getResponseHeader('X-Custom-Header')).toEqual('new-value') + + return new Response('OK') + }) + + const request = new Request('http://localhost:3000/test') + await handler(request, {}) + }) + + it('should handle multiple headers with the same name added via headers.append()', async () => { + const headers = new Headers() + headers.append('Set-Cookie', 'session=abc123; Path=/; HttpOnly') + headers.append('Set-Cookie', 'user=john; Path=/; Secure') + + const handler = requestHandler(() => { + setResponseHeaders(headers) + const setCookieValue = getResponseHeader('Set-Cookie') + + // When multiple values are appended with the same header name, + // the Headers API returns them comma-separated when iterating with entries() + // The current implementation uses .set() in a loop, so it should contain + // the comma-separated value from headers.entries() + expect(setCookieValue).toBeDefined() + + // Both cookie values should be present in the result + // (either comma-separated from a single iteration, or from multiple iterations) + expect(setCookieValue).toContain('session=abc123') + expect(setCookieValue).toContain('user=john') + + return new Response('OK') + }) + + const request = new Request('http://localhost:3000/test') + await handler(request, {}) + }) +}) From 2d6e6008fa785501b400251ea2db625932e2a1f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Gonz=C3=A1lez?= Date: Sun, 19 Oct 2025 12:21:33 -0600 Subject: [PATCH 2/2] fix: correct comment about Headers.entries() behavior --- .../start-server-core/tests/request-response.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/start-server-core/tests/request-response.test.ts b/packages/start-server-core/tests/request-response.test.ts index 16edf422de1..8edf78739e8 100644 --- a/packages/start-server-core/tests/request-response.test.ts +++ b/packages/start-server-core/tests/request-response.test.ts @@ -87,16 +87,17 @@ describe('setResponseHeaders', () => { const handler = requestHandler(() => { setResponseHeaders(headers) - const setCookieValue = getResponseHeader('Set-Cookie') // When multiple values are appended with the same header name, - // the Headers API returns them comma-separated when iterating with entries() - // The current implementation uses .set() in a loop, so it should contain - // the comma-separated value from headers.entries() + // headers.entries() returns separate entries for each value. + // The implementation uses .set() for the first occurrence and .append() for + // subsequent duplicates, preserving all values. + // Note: getResponseHeader() uses .get() which returns comma-separated values. + const setCookieValue = getResponseHeader('Set-Cookie') + expect(setCookieValue).toBeDefined() // Both cookie values should be present in the result - // (either comma-separated from a single iteration, or from multiple iterations) expect(setCookieValue).toContain('session=abc123') expect(setCookieValue).toContain('user=john')