Cookies().GetAll() Does Not Return All Duplicate Cookies With Same Name
In Next.js App Router, when a request contains multiple cookies with the same name, cookies().getAll() returns only the last cookie instead of all matching cookies, contrary to documentation. The issue stems from cookie parsing that uses a Map-like structure, which overwrites duplicate names.
Next.js internally parses the Cookie header using a key-value store (likely `cookie.parse` from the `cookie` package) that does not preserve duplicate names. The `getAll` method then retrieves values from this store, resulting in only the last value for duplicate names. The underlying design flaw is that HTTP cookies allow multiple cookies with the same name (though with different paths or domains), and the API should expose all of them.
1. Create a Next.js app with a route handler that returns JSON containing `(await cookies()).getAll()` and `(await cookies()).getAll('sessionid')`. 2. Start the app. 3. Send a request with header `Cookie: sessionid=a; sessionid=b`. 4. Observe that the response includes only `sessionid=b` for both calls.
Fixing Code Block
// packages/next/src/server/web/spec-extension/adapters/request-cookies.ts
// Replace the existing parseCookieString function with this one that
// preserves duplicate cookie names.
export function parseCookieString(cookieString: string): RequestCookie[] {
const cookies: RequestCookie[] = []
const parts = cookieString.split(';')
for (const part of parts) {
const trimmed = part.trim()
if (!trimmed) continue
const eqIndex = trimmed.indexOf('=')
if (eqIndex === -1) {
cookies.push({ name: trimmed, value: '' })
} else {
const name = trimmed.slice(0, eqIndex)
const value = trimmed.slice(eqIndex + 1)
cookies.push({ name, value })
}
}
return cookies
}
// Modify the RequestCookies class to store an array instead of a Map.
export class RequestCookies {
private _cookies: RequestCookie[]
constructor(cookies: RequestCookie[] = []) {
this._cookies = cookies
}
get(name: string): RequestCookie | undefined {
const matches = this._cookies.filter(cookie => cookie.name === name)
return matches[matches.length - 1] // Preserve existing behavior of returning the last one
}
getAll(name?: string): RequestCookie[] {
if (name) {
return this._cookies.filter(cookie => cookie.name === name)
}
return this._cookies.slice()
}
has(name: string): boolean {
return this._cookies.some(cookie => cookie.name === name)
}
set(name: string, value: string, options?: Partial<RequestCookie>): this {
// For response cookies, this may need to be handled separately.
// This simple implementation appends a new cookie, but in practice,
// response cookie management should use a separate Map to avoid duplicates.
const newCookie: RequestCookie = { name, value, ...options }
const index = this._cookies.findIndex(cookie => cookie.name === name)
if (index !== -1) {
// Remove all existing with same name? Typically set replaces one cookie,
// but for simplicity, we replace the last one.
const lastIndex = this._cookies.map(cookie => cookie.name).lastIndexOf(name)
this._cookies[lastIndex] = newCookie
} else {
this._cookies.push(newCookie)
}
return this
}
delete(name: string): boolean {
const initialLength = this._cookies.length
this._cookies = this._cookies.filter(cookie => cookie.name !== name)
return this._cookies.length !== initialLength
}
clear(): void {
this._cookies = []
}
[Symbol.iterator](): Iterator<[string, RequestCookie]> {
return this.entries()
}
*entries(): Iterator<[string, RequestCookie]> {
for (const cookie of this._cookies) {
yield [cookie.name, cookie]
}
}
*keys(): Iterator<string> {
for (const cookie of this._cookies) {
yield cookie.name
}
}
*values(): Iterator<RequestCookie> {
for (const cookie of this._cookies) {
yield cookie
}
}
toString(): string {
return this._cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; ')
}
}
The fix changes the internal cookie storage from a Map (which loses duplicate names) to an array of `RequestCookie` objects. The `get` method returns the last matching cookie (preserving previous behavior), while `getAll` returns all matches. The parser is rewritten to split the Cookie header manually instead of using `cookie.parse`, which collapses duplicates. This aligns with the documented behavior.
Edge Case Audit
This modification changes the internal data structure of RequestCookies, which could affect other parts of Next.js that rely on Map-like behavior (e.g., `has`, `set`, `delete`). In particular, `set` and `delete` logic needs careful review to avoid unintended side effects when multiple cookies with the same name exist. The provided implementation is a minimal patch and may not fully support response cookie mutation; rollback to the original file if unexpected issues occur. Also, this change may alter the order of cookies returned by `getAll()` (now insertion order, not last-write-wins). Thorough testing is required in both Server Components and Route Handlers, especially for concurrent requests and cookie mutations.