Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
51f9667
delete x-middleware-set-cookie after runMiddleware executes
abhi12299 Sep 9, 2024
c7000e1
remove x-middleware-set-cookie in web/sandbox file instead of next-se…
abhi12299 Sep 10, 2024
68b9a73
mark cookiesFromResponse as string array since we receive an array fr…
abhi12299 Sep 10, 2024
62bfe78
deduplicate cookies based on names, prioritise downstream cookies ins…
abhi12299 Sep 10, 2024
c8042ca
add getCookies function in BrowserInterface and playwright
abhi12299 Sep 10, 2024
cd3729d
add e2e test for deduplication of cookies
abhi12299 Sep 10, 2024
c6028ee
Merge branch 'canary' of github.com:vercel/next.js into dedup-cookies
abhi12299 Sep 10, 2024
87cbe15
add whitespace back
abhi12299 Sep 10, 2024
614de35
Merge branch 'canary' into dedup-cookies
abhi12299 Sep 15, 2024
646300f
Merge branch 'canary' into dedup-cookies
abhi12299 Sep 16, 2024
ed0c56e
Merge branch 'vercel:canary' into dedup-cookies
abhi12299 Sep 17, 2024
4820df2
fix patchSetHeader implementation
abhi12299 Sep 17, 2024
78b5dc4
replace getCookies with waitForResponse function in playwright
abhi12299 Sep 17, 2024
0d68564
fix test for cookies-dedup by checking HTTP response instead of brows…
abhi12299 Sep 17, 2024
14342b8
Merge branch 'canary' into dedup-cookies
abhi12299 Sep 17, 2024
4341829
Merge branch 'canary' into dedup-cookies
ztanner Sep 18, 2024
eb1d316
increase timeout in dev mode for waitForResponse
abhi12299 Sep 18, 2024
0f228bd
mergeMiddlewareCookie now uses getRequestMeta to obtain middleware co…
abhi12299 Sep 18, 2024
7386e55
Merge branch 'canary' into dedup-cookies
abhi12299 Sep 18, 2024
a1b74c7
Merge branch 'canary' into dedup-cookies
abhi12299 Sep 18, 2024
df22e72
trigger CI
abhi12299 Sep 18, 2024
23fea62
Merge branch 'canary' of github.com:vercel/next.js into dedup-cookies
abhi12299 Sep 19, 2024
cd89568
Merge branch 'canary' of github.com:abhi12299/next.js into dedup-cookies
abhi12299 Sep 29, 2024
2d66bc8
revert changes for x-middleware-set-cookie and mergeMiddlewareCookies…
abhi12299 Sep 30, 2024
473cd0f
add await cookies() in action
abhi12299 Oct 4, 2024
05cbac8
waitForResponse use default timeout value
abhi12299 Oct 4, 2024
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
41 changes: 31 additions & 10 deletions packages/next/src/server/lib/patch-set-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ type PatchableResponse = {

/**
* Ensure cookies set in middleware are merged and not overridden by API
* routes/getServerSideProps.
* routes/getServerSideProps. Since middleware always runs before API routes
* and actions, any duplicate cookie set downstream are given priority
* and overlapping cookie names set by middleware are be removed.
*
* @param req Incoming request
* @param res Outgoing response
Expand All @@ -28,22 +30,41 @@ export function patchSetHeaderWithCookieSupport(

if (name.toLowerCase() === 'set-cookie') {
const middlewareValue = getRequestMeta(req, 'middlewareCookie')

if (
!middlewareValue ||
!Array.isArray(value) ||
!value.every((item, idx) => item === middlewareValue[idx])
) {
let valueAsArray: string[] =
typeof value === 'string'
? [value]
: Array.isArray(value)
? value
: []

// this step is necessary because when merging cookies in send-response.ts
// the "value" argument will contain the cookies set by the middleware
// so we remove those in this step by exact match
valueAsArray = valueAsArray.filter(
(cookie) => !middlewareValue?.includes(cookie)
)
const cookieNamesToSet = new Set<string>()
for (const cookie of valueAsArray) {
const [cookieName] = cookie.split('=')
cookieNamesToSet.add(cookieName.trim())
}

const middlewareCookiesToSet = []
for (const mwCookie of middlewareValue || []) {
const [cookieName] = mwCookie.split('=')
if (!cookieNamesToSet.has(cookieName.trim())) {
middlewareCookiesToSet.push(mwCookie)
}
}

value = [
// TODO: (wyattjoh) find out why this is called multiple times resulting in duplicate cookies being added
...new Set([
...(middlewareValue || []),
...(typeof value === 'string'
? [value]
: Array.isArray(value)
? value
: []),
]),
...new Set([...middlewareCookiesToSet, ...valueAsArray]),
]
}
}
Expand Down
11 changes: 8 additions & 3 deletions packages/next/src/server/web/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export async function adapter(

const event = new NextFetchEvent({ request, page: params.page })
let response
let cookiesFromResponse
let cookiesFromResponse: string[] | undefined

response = await propagator(request, () => {
// we only care to make async storage available for middleware
Expand Down Expand Up @@ -242,7 +242,10 @@ export async function adapter(
url: request.nextUrl,
renderOpts: {
onUpdateCookies: (cookies) => {
cookiesFromResponse = cookies
if (!cookiesFromResponse) {
cookiesFromResponse = []
}
cookiesFromResponse.push(...cookies)
},
previewProps,
waitUntil,
Expand Down Expand Up @@ -279,7 +282,9 @@ export async function adapter(
}

if (response && cookiesFromResponse) {
response.headers.set('set-cookie', cookiesFromResponse)
for (const cookie of cookiesFromResponse) {
response.headers.append('set-cookie', cookie)
}
}

/**
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/app-dir/cookies-dedup/app/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use server'

import { cookies } from 'next/headers'

export async function cookieAction() {
const cookieStore = await cookies()
cookieStore.set('common-cookie', 'from-action')
}
6 changes: 6 additions & 0 deletions test/e2e/app-dir/cookies-dedup/app/api/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export async function GET() {
return new Response('', {
status: 200,
headers: { 'Set-Cookie': `common-cookie=from-api; Path=/` },
})
}
9 changes: 9 additions & 0 deletions test/e2e/app-dir/cookies-dedup/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'

export default function Root({ children }: { children: React.ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
21 changes: 21 additions & 0 deletions test/e2e/app-dir/cookies-dedup/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client'

import React from 'react'
import { cookieAction } from './actions'

export default function Page() {
const api = async () => {
await fetch('/api')
}

return (
<>
<button id="action" onClick={() => cookieAction()}>
click
</button>
<button id="api" onClick={() => api()}>
click
</button>
</>
)
}
35 changes: 35 additions & 0 deletions test/e2e/app-dir/cookies-dedup/cookies-dedup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { nextTestSetup } from 'e2e-utils'

describe('cookies-dedup', () => {
const { next } = nextTestSetup({
files: __dirname,
})

it('cookies set by middleware should be removed if action sets the same cookie', async () => {
const browser = await next.browser('/')
const url = await browser.url()
await browser.waitForElementByCss('button#action')

const actionResponsePromise = browser.waitForResponse(url)
await browser.elementByCss('button#action').click()
const actionResponse = await actionResponsePromise

const headers = await actionResponse.allHeaders()
const setCookieHeaders = headers['set-cookie']
expect(setCookieHeaders).toEqual('common-cookie=from-action; Path=/')
Copy link
Member

@ztanner ztanner Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this isn't working as expected when deployed (x-ref)... that's one tricky part about these middleware tests.

I've deployed with your changes here so you can see what I mean: https://vtest314-e2e-tests-67c8g3e0e-ztanner.vercel.app/

I think we might need to dig a bit more into what's happening here. One way you can test with your patched version of next.js is to:

  • pnpm build in the root monorepo
  • pnpm pack in packages/next
  • move the next.js tarball into your test app and add it as a dependency to your package.json, eg:
    "next": "file:./packed-next.tgz"
  • deploy the app to Vercel

I can also try and take a closer look at this in case you're stuck or don't have the bandwidth.

})

it('cookies set by middleware should be removed if api route sets the same cookie', async () => {
const browser = await next.browser('/')
const url = await browser.url()
await browser.waitForElementByCss('button#api')

const apiResponsePromise = browser.waitForResponse(`${url}api`)
await browser.elementByCss('button#api').click()
const apiResponse = await apiResponsePromise

const headers = await apiResponse.allHeaders()
const setCookieHeaders = headers['set-cookie']
expect(setCookieHeaders).toEqual('common-cookie=from-api; Path=/')
})
})
7 changes: 7 additions & 0 deletions test/e2e/app-dir/cookies-dedup/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { NextResponse } from 'next/server'

export default function () {
const response = NextResponse.next()
response.cookies.set('common-cookie', 'from-middleware')
return response
}
6 changes: 6 additions & 0 deletions test/e2e/app-dir/cookies-dedup/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}

module.exports = nextConfig
11 changes: 11 additions & 0 deletions test/lib/browsers/base.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Response } from 'playwright'

export type Event = 'request' | 'response'

/**
Expand Down Expand Up @@ -110,4 +112,13 @@ export abstract class BrowserInterface<TCurrent = any> {
abstract websocketFrames(): Promise<any[]>
abstract url(): Promise<string>
abstract waitForIdleNetwork(): Promise<void>
abstract waitForResponse(
urlOrPredicate:
| string
| RegExp
| ((response: Response) => boolean | Promise<boolean>),
options?: {
timeout?: number
}
): Promise<Response>
}
15 changes: 15 additions & 0 deletions test/lib/browsers/playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Page,
ElementHandle,
devices,
Response,
} from 'playwright'
import path from 'path'

Expand Down Expand Up @@ -491,4 +492,18 @@ export class Playwright extends BrowserInterface {
return page.waitForLoadState('networkidle')
})
}

waitForResponse(
urlOrPredicate:
| string
| RegExp
| ((response: Response) => boolean | Promise<boolean>),
options?: {
timeout?: number
}
): Promise<Response> {
return this.chain(() => {
return page.waitForResponse(urlOrPredicate, options)
})
}
}
Loading