diff --git a/packages/next/src/server/send-response.ts b/packages/next/src/server/send-response.ts index 070b400e7d053..c62c228d306ad 100644 --- a/packages/next/src/server/send-response.ts +++ b/packages/next/src/server/send-response.ts @@ -27,6 +27,13 @@ export async function sendResponse( res.statusCode = response.status res.statusMessage = response.statusText + // can add more headers to this list if needed + const headersWithMultipleValuesAllowed = [ + 'set-cookie', + 'www-authenticate', + 'proxy-authenticate', + ] + // Copy over the response headers. response.headers?.forEach((value, name) => { // The append handling is special cased for `set-cookie`. @@ -36,7 +43,15 @@ export async function sendResponse( res.appendHeader(name, cookie) } } else { - res.appendHeader(name, value) + // only append the header if it is either not present in the outbound response + // or if the header supports multiple values + const isHeaderPresent = typeof res.getHeader(name) !== 'undefined' + if ( + headersWithMultipleValuesAllowed.includes(name.toLowerCase()) || + !isHeaderPresent + ) { + res.appendHeader(name, value) + } } }) diff --git a/test/e2e/app-dir/no-duplicate-headers-middleware/app/favicon.ico b/test/e2e/app-dir/no-duplicate-headers-middleware/app/favicon.ico new file mode 100644 index 0000000000000..718d6fea4835e Binary files /dev/null and b/test/e2e/app-dir/no-duplicate-headers-middleware/app/favicon.ico differ diff --git a/test/e2e/app-dir/no-duplicate-headers-middleware/app/layout.tsx b/test/e2e/app-dir/no-duplicate-headers-middleware/app/layout.tsx new file mode 100644 index 0000000000000..888614deda3ba --- /dev/null +++ b/test/e2e/app-dir/no-duplicate-headers-middleware/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/no-duplicate-headers-middleware/app/page.tsx b/test/e2e/app-dir/no-duplicate-headers-middleware/app/page.tsx new file mode 100644 index 0000000000000..ff7159d9149fe --- /dev/null +++ b/test/e2e/app-dir/no-duplicate-headers-middleware/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/app-dir/no-duplicate-headers-middleware/middleware.ts b/test/e2e/app-dir/no-duplicate-headers-middleware/middleware.ts new file mode 100644 index 0000000000000..6de9184fcf290 --- /dev/null +++ b/test/e2e/app-dir/no-duplicate-headers-middleware/middleware.ts @@ -0,0 +1,13 @@ +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' + +export function middleware(request: NextRequest) { + if (request.nextUrl.pathname === '/favicon.ico') { + return NextResponse.next({ + headers: { + 'Cache-Control': 'max-age=1234', + 'Content-Type': 'image/vnd.microsoft.icon', + }, + }) + } +} diff --git a/test/e2e/app-dir/no-duplicate-headers-middleware/next.config.js b/test/e2e/app-dir/no-duplicate-headers-middleware/next.config.js new file mode 100644 index 0000000000000..807126e4cf0bf --- /dev/null +++ b/test/e2e/app-dir/no-duplicate-headers-middleware/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/no-duplicate-headers-middleware/no-duplicate-headers-middleware.test.ts b/test/e2e/app-dir/no-duplicate-headers-middleware/no-duplicate-headers-middleware.test.ts new file mode 100644 index 0000000000000..34c99d876200e --- /dev/null +++ b/test/e2e/app-dir/no-duplicate-headers-middleware/no-duplicate-headers-middleware.test.ts @@ -0,0 +1,14 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('no-duplicate-headers-next-config', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should prioritise headers in middleware for static assets', async () => { + const res = await next.fetch('favicon.ico') + expect(res.status).toBe(200) + expect(res.headers.get('cache-control')).toBe('max-age=1234') + expect(res.headers.get('content-type')).toBe('image/vnd.microsoft.icon') + }) +}) diff --git a/test/e2e/app-dir/no-duplicate-headers-next-config/app/favicon.ico b/test/e2e/app-dir/no-duplicate-headers-next-config/app/favicon.ico new file mode 100644 index 0000000000000..718d6fea4835e Binary files /dev/null and b/test/e2e/app-dir/no-duplicate-headers-next-config/app/favicon.ico differ diff --git a/test/e2e/app-dir/no-duplicate-headers-next-config/app/layout.tsx b/test/e2e/app-dir/no-duplicate-headers-next-config/app/layout.tsx new file mode 100644 index 0000000000000..888614deda3ba --- /dev/null +++ b/test/e2e/app-dir/no-duplicate-headers-next-config/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/no-duplicate-headers-next-config/app/page.tsx b/test/e2e/app-dir/no-duplicate-headers-next-config/app/page.tsx new file mode 100644 index 0000000000000..ff7159d9149fe --- /dev/null +++ b/test/e2e/app-dir/no-duplicate-headers-next-config/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/app-dir/no-duplicate-headers-next-config/next.config.js b/test/e2e/app-dir/no-duplicate-headers-next-config/next.config.js new file mode 100644 index 0000000000000..75be548b9f41c --- /dev/null +++ b/test/e2e/app-dir/no-duplicate-headers-next-config/next.config.js @@ -0,0 +1,24 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + async headers() { + return [ + { + source: '/favicon.ico', + headers: [ + { + key: 'cache-control', + value: 'max-age=1234', + }, + { + key: 'content-type', + value: 'image/vnd.microsoft.icon', + }, + ], + }, + ] + }, +} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/no-duplicate-headers-next-config/no-duplicate-headers-next-config.test.ts b/test/e2e/app-dir/no-duplicate-headers-next-config/no-duplicate-headers-next-config.test.ts new file mode 100644 index 0000000000000..b5e0c6d4e295e --- /dev/null +++ b/test/e2e/app-dir/no-duplicate-headers-next-config/no-duplicate-headers-next-config.test.ts @@ -0,0 +1,14 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('no-duplicate-headers-next-config', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should prioritise headers in next config for static assets', async () => { + const res = await next.fetch('favicon.ico') + expect(res.status).toBe(200) + expect(res.headers.get('cache-control')).toBe('max-age=1234') + expect(res.headers.get('content-type')).toBe('image/vnd.microsoft.icon') + }) +})