diff --git a/src/utils/middleware-cookies.ts b/src/utils/middleware-cookies.ts index d593bfdf3..fbe23533a 100644 --- a/src/utils/middleware-cookies.ts +++ b/src/utils/middleware-cookies.ts @@ -4,11 +4,14 @@ import { NextRequest, NextResponse } from 'next/server'; export default class MiddlewareCookies extends Cookies { protected getSetCookieHeader(res: NextResponse): string[] { const value = res.headers.get('set-cookie'); - return value?.split(', ') || []; + return splitCookiesString(value as string); } protected setSetCookieHeader(res: NextResponse, cookies: string[]): void { - res.headers.set('set-cookie', cookies.join(', ')); + res.headers.delete('set-cookie'); + for (const cookie of cookies) { + res.headers.append('set-cookie', cookie); + } } getAll(req: NextRequest): Record { @@ -24,3 +27,76 @@ export default class MiddlewareCookies extends Cookies { }, {}); } } + +/* eslint-disable max-len */ +/** + * Handle cookies with commas, eg `foo=; Expires=Thu, 01 Jan 1970 00:00:00 GMT` + * @source https://github.com/vercel/edge-runtime/blob/90160abc42e6139c41494c5d2e98f09e9a5fa514/packages/cookies/src/response-cookies.ts#L128 + */ +function splitCookiesString(cookiesString: string) { + if (!cookiesString) return []; + const cookiesStrings = []; + let pos = 0; + let start; + let ch; + let lastComma; + let nextStart; + let cookiesSeparatorFound; + + function skipWhitespace() { + while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) { + pos += 1; + } + return pos < cookiesString.length; + } + + function notSpecialChar() { + ch = cookiesString.charAt(pos); + + return ch !== '=' && ch !== ';' && ch !== ','; + } + + while (pos < cookiesString.length) { + start = pos; + cookiesSeparatorFound = false; + + while (skipWhitespace()) { + ch = cookiesString.charAt(pos); + if (ch === ',') { + // ',' is a cookie separator if we have later first '=', not ';' or ',' + lastComma = pos; + pos += 1; + + skipWhitespace(); + nextStart = pos; + + while (pos < cookiesString.length && notSpecialChar()) { + pos += 1; + } + + // currently special character + if (pos < cookiesString.length && cookiesString.charAt(pos) === '=') { + // we found cookies separator + cookiesSeparatorFound = true; + // pos is inside the next cookie, so back up and return it. + pos = nextStart; + cookiesStrings.push(cookiesString.substring(start, lastComma)); + start = pos; + /* c8 ignore next 5 */ + } else { + // in param ',' or param separator ';', + // we continue from that comma + pos = lastComma + 1; + } + } else { + pos += 1; + } + } + + if (!cookiesSeparatorFound || pos >= cookiesString.length) { + cookiesStrings.push(cookiesString.substring(start, cookiesString.length)); + } + } + + return cookiesStrings; +} diff --git a/tests/utils/middleware-cookies.test.ts b/tests/utils/middleware-cookies.test.ts index 7ee1e01cb..e539b6154 100644 --- a/tests/utils/middleware-cookies.test.ts +++ b/tests/utils/middleware-cookies.test.ts @@ -3,6 +3,7 @@ */ import MiddlewareCookies from '../../src/utils/middleware-cookies'; import { NextRequest, NextResponse } from 'next/server'; +import { serialize } from 'cookie'; const setup = (reqInit?: RequestInit): [NextRequest, NextResponse] => { return [new NextRequest(new URL('http://example.com'), reqInit), NextResponse.next()]; @@ -54,6 +55,20 @@ describe('cookie', () => { expect(res.headers.get('set-cookie')).toEqual(['foo=bar', 'baz=qux'].join(', ')); }); + it('should not overwrite existing set cookie with expiry', async () => { + const [, res] = setup(); + res.headers.set('set-cookie', serialize('foo', '', { expires: new Date(0) })); + const setter = new MiddlewareCookies(); + setter.set('baz', 'qux'); + setter.commit(res); + expect(res.headers.get('set-cookie')).toEqual( + ['foo=; Expires=Thu, 01 Jan 1970 00:00:00 GMT', 'baz=qux'].join(', ') + ); + if ('getAll' in res.headers) { + expect((res.headers.getAll as (header: string) => string[])('set-cookie')).toHaveLength(2); + } + }); + it('should override existing cookies that equal name', async () => { const [, res] = setup(); res.headers.set('set-cookie', ['foo=bar', 'baz=qux'].join(', '));