Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions packages/start-server-core/src/request-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,16 @@ export function setResponseHeaders(
headers: TypedHeaders<ResponseHeaderMap>,
): void {
const event = getH3Event()
for (const [name, value] of Object.entries(headers)) {
event.res.headers.set(name, value)
const addedHeaderNames: Record<string, true> = {}
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)
}
}

Expand Down
110 changes: 110 additions & 0 deletions packages/start-server-core/tests/request-response.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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)

// When multiple values are appended with the same header name,
// 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
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, {})
})
})