diff --git a/packages/next/server/web/spec-extension/cookies.ts b/packages/next/server/web/spec-extension/cookies.ts index 1072d4a1e1a3..3d60c5499d22 100644 --- a/packages/next/server/web/spec-extension/cookies.ts +++ b/packages/next/server/web/spec-extension/cookies.ts @@ -19,6 +19,23 @@ const normalizeCookieOptions = (options: CookieSerializeOptions) => { const serializeValue = (value: unknown) => typeof value === 'object' ? `j:${JSON.stringify(value)}` : String(value) +const serializeExpiredCookie = ( + key: string, + options: CookieSerializeOptions = {} +) => + cookie.serialize(key, '', { + expires: new Date(0), + path: '/', + ...options, + }) + +const deserializeCookie = (input: Request | Response): string[] => { + const value = input.headers.get('set-cookie') + return value !== undefined && value !== null ? value.split(', ') : [] +} + +const serializeCookie = (input: string[]) => input.join(', ') + export class Cookies extends Map { constructor(input?: string | null) { const parsedInput = typeof input === 'string' ? cookie.parse(input) : {} @@ -36,13 +53,6 @@ export class Cookies extends Map { } } -const deserializeCookie = (input: Request | Response): string[] => { - const value = input.headers.get('set-cookie') - return value !== undefined && value !== null ? value.split(', ') : [] -} - -const serializeCookie = (input: string[]) => input.join(', ') - export class NextCookies extends Cookies { response: Request | Response @@ -64,7 +74,7 @@ export class NextCookies extends Cookies { if (setCookie) { this.response.headers.set( 'set-cookie', - `${store.get(args[0])}, ${setCookie}` + [store.get(args[0]), setCookie].join(', ') ) } else { this.response.headers.set('set-cookie', store.get(args[0])) @@ -75,7 +85,7 @@ export class NextCookies extends Cookies { return store } - delete(key: any) { + delete(key: any, options: CookieSerializeOptions = {}) { const isDeleted = super.delete(key) if (isDeleted) { @@ -84,18 +94,21 @@ export class NextCookies extends Cookies { (value) => !value.startsWith(`${key}=`) ) ) - - if (setCookie) { - this.response.headers.set('set-cookie', setCookie) - } else { - this.response.headers.delete('set-cookie') - } + const expiredCookie = serializeExpiredCookie(key, options) + this.response.headers.set( + 'set-cookie', + [expiredCookie, setCookie].join(', ') + ) } return isDeleted } - clear() { - this.response.headers.delete('set-cookie') + clear(options: CookieSerializeOptions = {}) { + const expiredCookies = Array.from(super.keys()) + .map((key) => serializeExpiredCookie(key, options)) + .join(', ') + + if (expiredCookies) this.response.headers.set('set-cookie', expiredCookies) return super.clear() } } diff --git a/test/unit/web-runtime/next-cookies.test.ts b/test/unit/web-runtime/next-cookies.test.ts index a650ee2f2069..604819ecd0ce 100644 --- a/test/unit/web-runtime/next-cookies.test.ts +++ b/test/unit/web-runtime/next-cookies.test.ts @@ -79,17 +79,17 @@ it('reflect .delete into `set-cookie`', async () => { const firstDelete = response.cookies.delete('foo') expect(firstDelete).toBe(true) expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( - 'fooz=barz; Path=/' + 'foo=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT, fooz=barz; Path=/' ) + expect(response.cookies.get('foo')).toBe(undefined) const secondDelete = response.cookies.delete('fooz') expect(secondDelete).toBe(true) expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( - undefined + 'fooz=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT, foo=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT' ) expect(response.cookies.get('fooz')).toBe(undefined) - expect(response.cookies.size).toBe(0) }) @@ -100,6 +100,11 @@ it('reflect .clear into `set-cookie`', async () => { const response = new NextResponse() + response.cookies.clear() + expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( + undefined + ) + response.cookies.set('foo', 'bar') expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( 'foo=bar; Path=/' @@ -113,7 +118,7 @@ it('reflect .clear into `set-cookie`', async () => { response.cookies.clear() expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( - undefined + 'foo=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT, fooz=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT' ) })