diff --git a/src/adapter/utils.ts b/src/adapter/utils.ts index e9eb1e73..0d889e6c 100644 --- a/src/adapter/utils.ts +++ b/src/adapter/utils.ts @@ -338,6 +338,7 @@ export const createResponseHandler = (handler: CreateHandlerParameter) => { return (response: Response, set: Context['set'], request?: Request) => { let isCookieSet = false + // Merge headers: Response headers take precedence, set.headers fill in non-conflicting ones if (set.headers instanceof Headers) for (const key of set.headers.keys()) { if (key === 'set-cookie') { @@ -347,14 +348,21 @@ export const createResponseHandler = (handler: CreateHandlerParameter) => { for (const cookie of set.headers.getSetCookie()) response.headers.append('set-cookie', cookie) - } else response.headers.append(key, set.headers?.get(key) ?? '') + } else if (!response.headers.has(key)) + response.headers.set(key, set.headers?.get(key) ?? '') } else for (const key in set.headers) - (response as Response).headers.append( - key, - set.headers[key] as any - ) + if (key === 'set-cookie') + (response as Response).headers.append( + key, + set.headers[key] as any + ) + else if (!response.headers.has(key)) + (response as Response).headers.set( + key, + set.headers[key] as any + ) const status = set.status ?? 200 diff --git a/test/response/custom-response.test.ts b/test/response/custom-response.test.ts index c166071d..95438183 100644 --- a/test/response/custom-response.test.ts +++ b/test/response/custom-response.test.ts @@ -36,4 +36,29 @@ describe('Custom Response Type', () => { expect(await response.text()).toBe('Shuba Shuba') }) + + it('Response headers take precedence, set.headers merge non-conflicting', async () => { + const app = new Elysia() + .onRequest(({ set }) => { + set.headers['Content-Type'] = 'application/json' + set.headers['X-Framework'] = 'Elysia' + }) + .get('/', () => { + return new Response('{"message":"hello"}', { + headers: { + 'Content-Type': 'text/plain', + 'X-Custom': 'custom-value' + } + }) + }) + + const response = await app.handle(req('/')) + + // Response's Content-Type takes precedence + expect(response.headers.get('Content-Type')).toBe('text/plain') + // set.headers adds non-conflicting headers + expect(response.headers.get('X-Framework')).toBe('Elysia') + // Response's own headers are preserved + expect(response.headers.get('X-Custom')).toBe('custom-value') + }) })