From 4a01f5c380f07d5a5b60d259c7bb4d9bd850abfc Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Mon, 25 Apr 2022 12:08:51 +0200 Subject: [PATCH 1/7] feat: better cookies API for NextResponse --- .../next/server/web/spec-extension/cookies.ts | 103 ++++++++++++++ .../next/server/web/spec-extension/request.ts | 14 +- .../server/web/spec-extension/response.ts | 44 +----- test/unit/web-runtime/cookies.test.ts | 101 ++++++++++++++ test/unit/web-runtime/next-cookies.test.ts | 129 ++++++++++++++++++ test/unit/web-runtime/next-response.test.ts | 11 -- 6 files changed, 343 insertions(+), 59 deletions(-) create mode 100644 packages/next/server/web/spec-extension/cookies.ts create mode 100644 test/unit/web-runtime/cookies.test.ts create mode 100644 test/unit/web-runtime/next-cookies.test.ts diff --git a/packages/next/server/web/spec-extension/cookies.ts b/packages/next/server/web/spec-extension/cookies.ts new file mode 100644 index 000000000000..1072d4a1e1a3 --- /dev/null +++ b/packages/next/server/web/spec-extension/cookies.ts @@ -0,0 +1,103 @@ +import cookie from 'next/dist/compiled/cookie' +import { CookieSerializeOptions } from '../types' + +const normalizeCookieOptions = (options: CookieSerializeOptions) => { + options = Object.assign({}, options) + + if (options.maxAge) { + options.expires = new Date(Date.now() + options.maxAge) + options.maxAge /= 1000 + } + + if (options.path == null) { + options.path = '/' + } + + return options +} + +const serializeValue = (value: unknown) => + typeof value === 'object' ? `j:${JSON.stringify(value)}` : String(value) + +export class Cookies extends Map { + constructor(input?: string | null) { + const parsedInput = typeof input === 'string' ? cookie.parse(input) : {} + super(Object.entries(parsedInput)) + } + set(key: string, value: unknown, options: CookieSerializeOptions = {}) { + return super.set( + key, + cookie.serialize( + key, + serializeValue(value), + normalizeCookieOptions(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 NextCookies extends Cookies { + response: Request | Response + + constructor(response: Request | Response) { + super(response.headers.get('cookie')) + this.response = response + } + set(...args: Parameters) { + const isAlreadyAdded = super.has(args[0]) + const store = super.set(...args) + + if (isAlreadyAdded) { + const setCookie = serializeCookie( + deserializeCookie(this.response).filter( + (value) => !value.startsWith(`${args[0]}=`) + ) + ) + + if (setCookie) { + this.response.headers.set( + 'set-cookie', + `${store.get(args[0])}, ${setCookie}` + ) + } else { + this.response.headers.set('set-cookie', store.get(args[0])) + } + } else { + this.response.headers.append('set-cookie', store.get(args[0])) + } + + return store + } + delete(key: any) { + const isDeleted = super.delete(key) + + if (isDeleted) { + const setCookie = serializeCookie( + deserializeCookie(this.response).filter( + (value) => !value.startsWith(`${key}=`) + ) + ) + + if (setCookie) { + this.response.headers.set('set-cookie', setCookie) + } else { + this.response.headers.delete('set-cookie') + } + } + + return isDeleted + } + clear() { + this.response.headers.delete('set-cookie') + return super.clear() + } +} + +export { CookieSerializeOptions } diff --git a/packages/next/server/web/spec-extension/request.ts b/packages/next/server/web/spec-extension/request.ts index b4b20424f116..39f67d41bab9 100644 --- a/packages/next/server/web/spec-extension/request.ts +++ b/packages/next/server/web/spec-extension/request.ts @@ -3,14 +3,15 @@ import type { RequestData } from '../types' import { NextURL } from '../next-url' import { isBot } from '../../utils' import { toNodeHeaders } from '../utils' -import cookie from 'next/dist/compiled/cookie' import parseua from 'next/dist/compiled/ua-parser-js' +import { NextCookies } from './cookies' + export const INTERNALS = Symbol('internal request') export class NextRequest extends Request { [INTERNALS]: { - cookieParser(): { [key: string]: string } + cookies: NextCookies geo: RequestData['geo'] ip?: string page?: { name?: string; params?: { [key: string]: string | string[] } } @@ -21,13 +22,8 @@ export class NextRequest extends Request { constructor(input: Request | string, init: RequestInit = {}) { super(input, init) - const cookieParser = () => { - const value = this.headers.get('cookie') - return value ? cookie.parse(value) : {} - } - this[INTERNALS] = { - cookieParser, + cookies: new NextCookies(this), geo: init.geo || {}, ip: init.ip, page: init.page, @@ -41,7 +37,7 @@ export class NextRequest extends Request { } public get cookies() { - return this[INTERNALS].cookieParser() + return this[INTERNALS].cookies } public get geo() { diff --git a/packages/next/server/web/spec-extension/response.ts b/packages/next/server/web/spec-extension/response.ts index a82224e6643d..8ab8b0877317 100644 --- a/packages/next/server/web/spec-extension/response.ts +++ b/packages/next/server/web/spec-extension/response.ts @@ -1,28 +1,23 @@ import type { I18NConfig } from '../../config-shared' import { NextURL } from '../next-url' import { toNodeHeaders, validateURL } from '../utils' -import cookie from 'next/dist/compiled/cookie' -import { CookieSerializeOptions } from '../types' + +import { NextCookies } from './cookies' const INTERNALS = Symbol('internal response') const REDIRECTS = new Set([301, 302, 303, 307, 308]) export class NextResponse extends Response { [INTERNALS]: { - cookieParser(): { [key: string]: string } + cookies: NextCookies url?: NextURL } constructor(body?: BodyInit | null, init: ResponseInit = {}) { super(body, init) - const cookieParser = () => { - const value = this.headers.get('cookie') - return value ? cookie.parse(value) : {} - } - this[INTERNALS] = { - cookieParser, + cookies: new NextCookies(this), url: init.url ? new NextURL(init.url, { basePath: init.nextConfig?.basePath, @@ -35,36 +30,7 @@ export class NextResponse extends Response { } public get cookies() { - return this[INTERNALS].cookieParser() - } - - public cookie( - name: string, - value: { [key: string]: any } | string, - opts: CookieSerializeOptions = {} - ) { - const val = - typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value) - - const options = { ...opts } - if (options.maxAge) { - options.expires = new Date(Date.now() + options.maxAge) - options.maxAge /= 1000 - } - - if (options.path == null) { - options.path = '/' - } - - this.headers.append( - 'Set-Cookie', - cookie.serialize(name, String(val), options) - ) - return this - } - - public clearCookie(name: string, opts: CookieSerializeOptions = {}) { - return this.cookie(name, '', { expires: new Date(1), path: '/', ...opts }) + return this[INTERNALS].cookies } static json(body: any, init?: ResponseInit) { diff --git a/test/unit/web-runtime/cookies.test.ts b/test/unit/web-runtime/cookies.test.ts new file mode 100644 index 000000000000..7ee620cd7018 --- /dev/null +++ b/test/unit/web-runtime/cookies.test.ts @@ -0,0 +1,101 @@ +/* eslint-env jest */ + +import { + Cookies, + CookieSerializeOptions, +} from 'next/dist/server/web/spec-extension/cookies' +import { Blob, File, FormData } from 'next/dist/compiled/formdata-node' +import { Headers } from 'next/dist/server/web/spec-compliant/headers' +import { Crypto } from 'next/dist/server/web/sandbox/polyfills' +import * as streams from 'web-streams-polyfill/ponyfill' + +beforeAll(() => { + global['Blob'] = Blob + global['crypto'] = new Crypto() + global['File'] = File + global['FormData'] = FormData + global['Headers'] = Headers + global['ReadableStream'] = streams.ReadableStream + global['TransformStream'] = streams.TransformStream +}) + +afterAll(() => { + delete global['Blob'] + delete global['crypto'] + delete global['File'] + delete global['Headers'] + delete global['FormData'] + delete global['ReadableStream'] + delete global['TransformStream'] +}) + +it('create a empty cookies bag', async () => { + const cookies = new Cookies() + expect(Object.entries(cookies)).toStrictEqual([]) +}) + +it('create a cookies bag from string', async () => { + const cookies = new Cookies('foo=bar; equation=E%3Dmc%5E2') + expect(Array.from(cookies.entries())).toStrictEqual([ + ['foo', 'foo=bar; Path=/'], + ['equation', 'equation=E%3Dmc%5E2; Path=/'], + ]) +}) + +it('.set', async () => { + const cookies = new Cookies() + cookies.set('foo', 'bar') + expect(Array.from(cookies.entries())).toStrictEqual([ + ['foo', 'foo=bar; Path=/'], + ]) +}) + +it('.set with options', async () => { + const cookies = new Cookies() + + const options: CookieSerializeOptions = { + path: '/', + maxAge: 1000 * 60 * 60 * 24 * 7, + httpOnly: true, + sameSite: 'strict', + domain: 'example.com', + } + + cookies.set('foo', 'bar', options) + + expect(options).toStrictEqual({ + path: '/', + maxAge: 1000 * 60 * 60 * 24 * 7, + httpOnly: true, + sameSite: 'strict', + domain: 'example.com', + }) + + const [[key, value]] = Array.from(cookies.entries()) + const values = value.split('; ') + + expect(key).toBe('foo') + + expect(values).toStrictEqual([ + 'foo=bar', + 'Max-Age=604800', + 'Domain=example.com', + 'Path=/', + expect.stringContaining('Expires='), + 'HttpOnly', + 'SameSite=Strict', + ]) +}) + +it('.delete', async () => { + const cookies = new Cookies() + cookies.set('foo', 'bar') + cookies.delete('foo') + expect(Array.from(cookies.entries())).toStrictEqual([]) +}) + +it('.has', async () => { + const cookies = new Cookies() + cookies.set('foo', 'bar') + expect(cookies.has('foo')).toBe(true) +}) diff --git a/test/unit/web-runtime/next-cookies.test.ts b/test/unit/web-runtime/next-cookies.test.ts new file mode 100644 index 000000000000..a650ee2f2069 --- /dev/null +++ b/test/unit/web-runtime/next-cookies.test.ts @@ -0,0 +1,129 @@ +/* eslint-env jest */ + +import { Blob, File, FormData } from 'next/dist/compiled/formdata-node' +import { Crypto } from 'next/dist/server/web/sandbox/polyfills' +import { Response } from 'next/dist/server/web/spec-compliant/response' +import { Headers } from 'next/dist/server/web/spec-compliant/headers' +import * as streams from 'web-streams-polyfill/ponyfill' + +beforeAll(() => { + global['Blob'] = Blob + global['crypto'] = new Crypto() + global['File'] = File + global['FormData'] = FormData + global['Headers'] = Headers + global['ReadableStream'] = streams.ReadableStream + global['TransformStream'] = streams.TransformStream + global['Response'] = Response +}) + +afterAll(() => { + delete global['Blob'] + delete global['crypto'] + delete global['File'] + delete global['Headers'] + delete global['FormData'] + delete global['ReadableStream'] + delete global['TransformStream'] +}) + +it('reflect .set into `set-cookie`', async () => { + const { NextResponse } = await import( + 'next/dist/server/web/spec-extension/response' + ) + + const response = new NextResponse() + + response.cookies.set('foo', 'bar') + expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( + 'foo=bar; Path=/' + ) + expect(response.cookies.get('foo')).toBe('foo=bar; Path=/') + + response.cookies.set('foo', 'barz') + expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( + 'foo=barz; Path=/' + ) + expect(response.cookies.get('foo')).toBe('foo=barz; Path=/') + + response.cookies.set('fooz', 'barz') + expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( + 'foo=barz; Path=/, fooz=barz; Path=/' + ) + + response.cookies.set('foo', 'bar') + expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( + 'foo=bar; Path=/, fooz=barz; Path=/' + ) +}) + +it('reflect .delete into `set-cookie`', async () => { + const { NextResponse } = await import( + 'next/dist/server/web/spec-extension/response' + ) + + const response = new NextResponse() + + response.cookies.set('foo', 'bar') + expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( + 'foo=bar; Path=/' + ) + expect(response.cookies.get('foo')).toBe('foo=bar; Path=/') + + response.cookies.set('fooz', 'barz') + expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( + 'foo=bar; Path=/, fooz=barz; Path=/' + ) + expect(response.cookies.get('fooz')).toBe('fooz=barz; Path=/') + + const firstDelete = response.cookies.delete('foo') + expect(firstDelete).toBe(true) + expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( + '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 + ) + expect(response.cookies.get('fooz')).toBe(undefined) + + expect(response.cookies.size).toBe(0) +}) + +it('reflect .clear into `set-cookie`', async () => { + const { NextResponse } = await import( + 'next/dist/server/web/spec-extension/response' + ) + + const response = new NextResponse() + + response.cookies.set('foo', 'bar') + expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( + 'foo=bar; Path=/' + ) + expect(response.cookies.get('foo')).toBe('foo=bar; Path=/') + + response.cookies.set('fooz', 'barz') + expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( + 'foo=bar; Path=/, fooz=barz; Path=/' + ) + + response.cookies.clear() + expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( + undefined + ) +}) + +it('response.cookie does not modify options', async () => { + const { NextResponse } = await import( + 'next/dist/server/web/spec-extension/response' + ) + + const options = { maxAge: 10000 } + const response = NextResponse.json(null) + response.cookies.set('cookieName', 'cookieValue', options) + expect(options).toEqual({ maxAge: 10000 }) +}) diff --git a/test/unit/web-runtime/next-response.test.ts b/test/unit/web-runtime/next-response.test.ts index 98fe67b08f24..85351b6e2212 100644 --- a/test/unit/web-runtime/next-response.test.ts +++ b/test/unit/web-runtime/next-response.test.ts @@ -71,14 +71,3 @@ it('automatically parses and formats JSON', async () => { body: '', }) }) - -it('response.cookie does not modify options', async () => { - const { NextResponse } = await import( - 'next/dist/server/web/spec-extension/response' - ) - - const options = { maxAge: 10000 } - const response = NextResponse.json(null) - response.cookie('cookieName', 'cookieValue', options) - expect(options).toEqual({ maxAge: 10000 }) -}) From 9d8d676785122dfd56a1d71eb94a3cb008309fea Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Wed, 27 Apr 2022 14:13:35 +0200 Subject: [PATCH 2/7] refactor: mark delete/clear cookies as expired --- .../next/server/web/spec-extension/cookies.ts | 47 ++++++++++++------- test/unit/web-runtime/next-cookies.test.ts | 13 +++-- 2 files changed, 39 insertions(+), 21 deletions(-) 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' ) }) From e8233dfd9ff6d727a7fff235a9d42added42cfc9 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 3 May 2022 10:26:12 +0200 Subject: [PATCH 3/7] refactor: setup maxAge in seconds --- packages/next/server/web/spec-extension/cookies.ts | 3 +-- test/unit/web-runtime/cookies.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/next/server/web/spec-extension/cookies.ts b/packages/next/server/web/spec-extension/cookies.ts index 3d60c5499d22..f6cf48029a7b 100644 --- a/packages/next/server/web/spec-extension/cookies.ts +++ b/packages/next/server/web/spec-extension/cookies.ts @@ -5,8 +5,7 @@ const normalizeCookieOptions = (options: CookieSerializeOptions) => { options = Object.assign({}, options) if (options.maxAge) { - options.expires = new Date(Date.now() + options.maxAge) - options.maxAge /= 1000 + options.expires = new Date(Date.now() + options.maxAge * 1000) } if (options.path == null) { diff --git a/test/unit/web-runtime/cookies.test.ts b/test/unit/web-runtime/cookies.test.ts index 7ee620cd7018..7eca76da0d45 100644 --- a/test/unit/web-runtime/cookies.test.ts +++ b/test/unit/web-runtime/cookies.test.ts @@ -55,7 +55,7 @@ it('.set with options', async () => { const options: CookieSerializeOptions = { path: '/', - maxAge: 1000 * 60 * 60 * 24 * 7, + maxAge: 60 * 60 * 24 * 7, httpOnly: true, sameSite: 'strict', domain: 'example.com', @@ -65,7 +65,7 @@ it('.set with options', async () => { expect(options).toStrictEqual({ path: '/', - maxAge: 1000 * 60 * 60 * 24 * 7, + maxAge: 60 * 60 * 24 * 7, httpOnly: true, sameSite: 'strict', domain: 'example.com', From a48783ba26caca115295eb8841599c5c46e3328b Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 3 May 2022 11:29:23 +0200 Subject: [PATCH 4/7] test: update interface --- .../middleware/core/pages/rewrites/_middleware.js | 6 +++--- .../middleware-typescript/app/pages/rewrites/_middleware.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/integration/middleware/core/pages/rewrites/_middleware.js b/test/integration/middleware/core/pages/rewrites/_middleware.js index f4018868045f..a4faeaf1fbec 100644 --- a/test/integration/middleware/core/pages/rewrites/_middleware.js +++ b/test/integration/middleware/core/pages/rewrites/_middleware.js @@ -25,12 +25,12 @@ export async function middleware(request) { } if (url.pathname === '/rewrites/rewrite-to-ab-test') { - let bucket = request.cookies.bucket + let bucket = request.cookies.get('bucket') if (!bucket) { bucket = Math.random() >= 0.5 ? 'a' : 'b' url.pathname = `/rewrites/${bucket}` const response = NextResponse.rewrite(url) - response.cookie('bucket', bucket, { maxAge: 10000 }) + response.cookies.set('bucket', bucket, { maxAge: 10 }) return response } @@ -73,7 +73,7 @@ export async function middleware(request) { ) { url.searchParams.set('middleware', 'foo') url.pathname = - request.cookies['about-bypass'] === '1' + request.cookies.get('about-bypass') === '1' ? '/rewrites/about-bypass' : '/rewrites/about' diff --git a/test/production/middleware-typescript/app/pages/rewrites/_middleware.ts b/test/production/middleware-typescript/app/pages/rewrites/_middleware.ts index 4a29591b4a11..402ce456671d 100644 --- a/test/production/middleware-typescript/app/pages/rewrites/_middleware.ts +++ b/test/production/middleware-typescript/app/pages/rewrites/_middleware.ts @@ -4,11 +4,11 @@ export const middleware: NextMiddleware = async function (request) { const url = request.nextUrl if (url.pathname === '/') { - let bucket = request.cookies.bucket + let bucket = request.cookies.get('bucket') if (!bucket) { bucket = Math.random() >= 0.5 ? 'a' : 'b' const response = NextResponse.rewrite(`/rewrites/${bucket}`) - response.cookie('bucket', bucket, { maxAge: 10000 }) + response.cookies.set('bucket', bucket, { maxAge: 10 }) return response } From 3a8b7f130d374795487d24e2106862905c76f265 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 3 May 2022 11:31:43 +0200 Subject: [PATCH 5/7] docs: update cookies reference --- docs/api-reference/next/server.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/api-reference/next/server.md b/docs/api-reference/next/server.md index 601be07c0d4d..66bd4dc0a1b8 100644 --- a/docs/api-reference/next/server.md +++ b/docs/api-reference/next/server.md @@ -35,7 +35,7 @@ The function can be a default export and as such, does **not** have to be named The `NextRequest` object is an extension of the native [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) interface, with the following added methods and properties: -- `cookies` - Has the cookies from the `Request` +- `cookies` - A [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) with cookies from the `Request` - `nextUrl` - Includes an extended, parsed, URL object that gives you access to Next.js specific properties such as `pathname`, `basePath`, `trailingSlash` and `i18n` - `ip` - Has the IP address of the `Request` - `ua` - Has the user agent @@ -69,9 +69,7 @@ The `NextResponse` class extends the native [`Response`](https://developer.mozil Public methods are available on an instance of the `NextResponse` class. Depending on your use case, you can create an instance and assign to a variable, then access the following public methods: -- `cookies` - An object with the cookies in the `Response` -- `cookie()` - Set a cookie in the `Response` -- `clearCookie()` - Accepts a `cookie` and clears it +- `cookies` - A [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) with the cookies in the `Response` ```ts import { NextResponse } from 'next/server' @@ -82,13 +80,13 @@ export function middleware(request: NextRequest) { // you could use `redirect()` or `rewrite()` as well let response = NextResponse.next() // get the cookies from the request - let cookieFromRequest = request.cookies['my-cookie'] + let cookieFromRequest = request.cookies.get('my-cookie') // set the `cookie` - response.cookie('hello', 'world') + response.cookies.set('hello', 'world') // set the `cookie` with options - const cookieWithOptions = response.cookie('hello', 'world', { + const cookieWithOptions = response.cookies.set('hello', 'world', { path: '/', - maxAge: 1000 * 60 * 60 * 24 * 7, + maxAge: 60 * 60 * 24 * 7, httpOnly: true, sameSite: 'strict', domain: 'example.com', @@ -146,7 +144,7 @@ import type { NextRequest } from 'next/server' export function middleware(req: NextRequest) { const res = NextResponse.redirect('/') // creates an actual instance - res.cookie('hello', 'world') // can be called on an instance + res.cookies.set('hello', 'world') // can be called on an instance return res } ``` From 9c8e9e59df741e39d5edf4c25b1d4cf2815d29ff Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Fri, 6 May 2022 16:45:52 +0200 Subject: [PATCH 6/7] fix: this reference --- packages/next/server/web/spec-extension/cookies.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next/server/web/spec-extension/cookies.ts b/packages/next/server/web/spec-extension/cookies.ts index f6cf48029a7b..791d0be5cf4b 100644 --- a/packages/next/server/web/spec-extension/cookies.ts +++ b/packages/next/server/web/spec-extension/cookies.ts @@ -59,7 +59,7 @@ export class NextCookies extends Cookies { super(response.headers.get('cookie')) this.response = response } - set(...args: Parameters) { + set = (...args: Parameters) => { const isAlreadyAdded = super.has(args[0]) const store = super.set(...args) @@ -84,7 +84,7 @@ export class NextCookies extends Cookies { return store } - delete(key: any, options: CookieSerializeOptions = {}) { + delete = (key: any, options: CookieSerializeOptions = {}) => { const isDeleted = super.delete(key) if (isDeleted) { @@ -102,7 +102,7 @@ export class NextCookies extends Cookies { return isDeleted } - clear(options: CookieSerializeOptions = {}) { + clear = (options: CookieSerializeOptions = {}) => { const expiredCookies = Array.from(super.keys()) .map((key) => serializeExpiredCookie(key, options)) .join(', ') From db5699106a2bc9df0217abf931bf7c1387d7d420 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Fri, 6 May 2022 17:30:10 +0200 Subject: [PATCH 7/7] fix: check for cookie presence --- .../middleware/core/pages/rewrites/_middleware.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/integration/middleware/core/pages/rewrites/_middleware.js b/test/integration/middleware/core/pages/rewrites/_middleware.js index a4faeaf1fbec..925c4dec9d46 100644 --- a/test/integration/middleware/core/pages/rewrites/_middleware.js +++ b/test/integration/middleware/core/pages/rewrites/_middleware.js @@ -72,10 +72,9 @@ export async function middleware(request) { url.searchParams.get('path') === 'rewrite-me-without-hard-navigation' ) { url.searchParams.set('middleware', 'foo') - url.pathname = - request.cookies.get('about-bypass') === '1' - ? '/rewrites/about-bypass' - : '/rewrites/about' + url.pathname = request.cookies.has('about-bypass') + ? '/rewrites/about-bypass' + : '/rewrites/about' const response = NextResponse.rewrite(url) response.headers.set('x-middleware-cache', 'no-cache')