Skip to content

Commit

Permalink
BREAKING CHANGE: feat(edge): split NextCookies to RequestCookies
Browse files Browse the repository at this point in the history
…and `ResponseCookies` (#41526)

Ref: [Slack
thread](https://vercel.slack.com/archives/C035J346QQL/p1666056382299069?thread_ts=1666041444.633059&cid=C035J346QQL),
[docs update](vercel/front#17090)

Spec: https://wicg.github.io/cookie-store/

BREAKING CHANGE:

Ref: vercel/edge-runtime#177,
vercel/edge-runtime#181

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
  • Loading branch information
balazsorban44 committed Oct 27, 2022
1 parent 0e25f8c commit 4d20beb
Show file tree
Hide file tree
Showing 19 changed files with 589 additions and 431 deletions.
40 changes: 24 additions & 16 deletions docs/advanced-features/middleware.md
Expand Up @@ -148,31 +148,39 @@ To produce a response from Middleware, you should `rewrite` to a route ([Page](/

## Using Cookies

The `cookies` API extends [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) and allows you to `get`, `set`, and `delete` cookies. It also includes methods like [entries](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries) and [values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries).
Cookies are regular headers. On a `Request`, they are stored in the `Cookie` header. On a `Response` they are in the `Set-Cookie` header. Next.js provides a convenient way to access and manipulate these cookies through the `cookies` extension on `NextRequest` and `NextResponse`.

1. For incoming requests, `cookies` comes with the following methods: `get`, `getAll`, `set`, and `delete` cookies. You can check for the existence of a cookie with `has` or remove all cookies with `clear`.
2. For outgoing responses, `cookies` have the following methods `get`, `getAll`, `set`, and `delete`.

```typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
// Setting cookies on the response
// Assume a "Cookie:vercel=fast" header to be present on the incoming request
// Getting cookies from the request using the `RequestCookies` API
const cookie = request.cookies.get('nextjs')?.value
console.log(cookie) // => 'fast'
const allCookies = request.cookies.getAll()
console.log(allCookies) // => [{ name: 'vercel', value: 'fast' }]

response.cookies.has('nextjs') // => true
response.cookies.delete('nextjs')
response.cookies.has('nextjs') // => false

// Setting cookies on the response using the `ResponseCookies` API
const response = NextResponse.next()
response.cookies.set('vercel', 'fast')
response.cookies.set('vercel', 'fast', { path: '/test' })

// Getting cookies from the request
const cookie = request.cookies.get('vercel')
console.log(cookie) // => 'fast'
const allCookies = request.cookies.entries()
console.log(allCookies) // => [{ key: 'vercel', value: 'fast' }]
const { value, options } = response.cookies.getWithOptions('vercel')
console.log(value) // => 'fast'
console.log(options) // => { Path: '/test' }

// Deleting cookies
response.cookies.delete('vercel')
response.cookies.clear()
response.cookies.set({
name: 'vercel',
value: 'fast',
path: '/test',
})
const cookie = response.cookies.get('vercel')
console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/test' }
// The outgoing response will have a `Set-Cookie:vercel=fast;path=/test` header.

return response
}
Expand Down
17 changes: 15 additions & 2 deletions docs/api-reference/next/server.md
Expand Up @@ -10,7 +10,15 @@ description: Learn about the server-only helpers for Middleware and Edge API Rou

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` - A [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) with cookies from the `Request`. See [Using cookies in Middleware](/docs/advanced-features/middleware#using-cookies)
- `cookies` - A [RequestCookies](https://edge-runtime.vercel.app/packages/cookies#for-request) instance with cookies from the `Request`. It reads/mutates the `Cookie` header of the request. See also [Using cookies in Middleware](/docs/advanced-features/middleware#using-cookies).

- `get` - A method that takes a cookie `name` and returns an object with `name` and `value`. If a cookie with `name` isn't found, it returns `undefined`. If multiple cookies match, it will only return the first match.
- `getAll` - A method that is similar to `get`, but returns a list of all the cookies with a matching `name`. If `name` is unspecified, it returns all the available cookies.
- `set` - A method that takes an object with properties of `CookieListItem` as defined in the [W3C CookieStore API](https://wicg.github.io/cookie-store/#dictdef-cookielistitem) spec.
- `delete` - A method that takes either a cookie `name` or a list of names. and removes the cookies matching the name(s). Returns `true` for deleted and `false` for undeleted cookies.
- `has` - A method that takes a cookie `name` and returns a `boolean` based on if the cookie exists (`true`) or not (`false`).
- `clear` - A method that takes no argument and will effectively remove the `Cookie` header.

- `nextUrl`: Includes an extended, parsed, URL object that gives you access to Next.js specific properties such as `pathname`, `basePath`, `trailingSlash` and `i18n`. Includes the following properties:
- `basePath` (`string`)
- `buildId` (`string || undefined`)
Expand Down Expand Up @@ -74,7 +82,12 @@ 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` - A [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) with the cookies in the `Response`
- `cookies` - A [ResponseCookies](https://edge-runtime.vercel.app/packages/cookies#for-response) instance with the cookies from the `Response`. It a
A [ResponseCooies](https://edge-runtime.vercel.app/packages/cookies#for-response) instance with cookies from the `Response`. It reads/mutates the `Set-Cookie` header of the response. See also [Using cookies in Middleware](/docs/advanced-features/middleware#using-cookies).
- `get` - A method that takes a cookie `name` and returns an object with `name` and `value`. If a cookie with `name` isn't found, it returns `undefined`. If multiple cookies match, it will only return the first match.
- `getAll` - A method that is similar to `get`, but returns a list of all the cookies with a matching `name`. If `name` is unspecified, it returns all the available cookies.
- `set` - A method that takes an object with properties of `CookieListItem` as defined in the [W3C CookieStore API](https://wicg.github.io/cookie-store/#dictdef-cookielistitem) spec.
- `delete` - A method that takes either a cookie `name` or a list of names. and removes the cookies matching the name(s). Returns `true` for deleted and `false` for undeleted cookies.

### Static Methods

Expand Down
7 changes: 2 additions & 5 deletions errors/middleware-upgrade-guide.md
Expand Up @@ -241,16 +241,13 @@ export function middleware() {
response.cookies.set('nextjs', 'awesome', { path: '/test' })

// get all the details of a cookie
const { value, options } = response.cookies.getWithOptions('vercel')
const { value, ...options } = response.cookies.getWithOptions('vercel')
console.log(value) // => 'fast'
console.log(options) // => { Path: '/test' }
console.log(options) // => { name: 'vercel', Path: '/test' }

// deleting a cookie will mark it as expired
response.cookies.delete('vercel')

// clear all cookies means mark all of them as expired
response.cookies.clear()

return response
}
```
Expand Down
9 changes: 6 additions & 3 deletions packages/next/client/components/request-async-storage.ts
@@ -1,9 +1,12 @@
import type { AsyncLocalStorage } from 'async_hooks'
import type { NextCookies } from '../../server/web/spec-extension/cookies'
import type {
ReadonlyHeaders,
ReadonlyRequestCookies,
} from '../../server/app-render'

export interface RequestStore {
headers: Headers
cookies: NextCookies
headers: ReadonlyHeaders
cookies: ReadonlyRequestCookies
previewData: any
}

Expand Down
44 changes: 19 additions & 25 deletions packages/next/server/app-render.tsx
Expand Up @@ -33,7 +33,7 @@ import { ServerInsertedHTMLContext } from '../shared/lib/server-inserted-html'
import { stripInternalQueries } from './internal-utils'
import type { ComponentsType } from '../build/webpack/loaders/next-app-loader'
import { REDIRECT_ERROR_CODE } from '../client/components/redirect'
import { NextCookies } from './web/spec-extension/cookies'
import { RequestCookies } from './web/spec-extension/cookies'
import { DYNAMIC_ERROR_CODE } from '../client/components/hooks-server-context'
import { NOT_FOUND_ERROR_CODE } from '../client/components/not-found'
import { HeadManagerContext } from '../shared/lib/head-manager-context'
Expand All @@ -45,7 +45,7 @@ const INTERNAL_HEADERS_INSTANCE = Symbol('internal for headers readonly')
function readonlyHeadersError() {
return new Error('ReadonlyHeaders cannot be modified')
}
class ReadonlyHeaders {
export class ReadonlyHeaders {
[INTERNAL_HEADERS_INSTANCE]: Headers

entries: Headers['entries']
Expand Down Expand Up @@ -83,20 +83,17 @@ class ReadonlyHeaders {
}

const INTERNAL_COOKIES_INSTANCE = Symbol('internal for cookies readonly')
function readonlyCookiesError() {
return new Error('ReadonlyCookies cannot be modified')
class ReadonlyRequestCookiesError extends Error {
message =
'ReadonlyRequestCookies cannot be modified. Read more: https://nextjs.org/api-reference/cookies'
}

class ReadonlyNextCookies {
[INTERNAL_COOKIES_INSTANCE]: NextCookies
export class ReadonlyRequestCookies {
[INTERNAL_COOKIES_INSTANCE]: RequestCookies

entries: NextCookies['entries']
forEach: NextCookies['forEach']
get: NextCookies['get']
getWithOptions: NextCookies['getWithOptions']
has: NextCookies['has']
keys: NextCookies['keys']
values: NextCookies['values']
get: RequestCookies['get']
getAll: RequestCookies['getAll']
has: RequestCookies['has']

constructor(request: {
headers: {
Expand All @@ -105,29 +102,26 @@ class ReadonlyNextCookies {
}) {
// Since `new Headers` uses `this.append()` to fill the headers object ReadonlyHeaders can't extend from Headers directly as it would throw.
// Request overridden to not have to provide a fully request object.
const cookiesInstance = new NextCookies(request as Request)
const cookiesInstance = new RequestCookies(request.headers as Headers)
this[INTERNAL_COOKIES_INSTANCE] = cookiesInstance

this.entries = cookiesInstance.entries.bind(cookiesInstance)
this.forEach = cookiesInstance.forEach.bind(cookiesInstance)
this.get = cookiesInstance.get.bind(cookiesInstance)
this.getWithOptions = cookiesInstance.getWithOptions.bind(cookiesInstance)
this.getAll = cookiesInstance.getAll.bind(cookiesInstance)
this.has = cookiesInstance.has.bind(cookiesInstance)
this.keys = cookiesInstance.keys.bind(cookiesInstance)
this.values = cookiesInstance.values.bind(cookiesInstance)
}

[Symbol.iterator]() {
return this[INTERNAL_COOKIES_INSTANCE][Symbol.iterator]()
return (this[INTERNAL_COOKIES_INSTANCE] as any)[Symbol.iterator]()
}

clear() {
throw readonlyCookiesError()
throw new ReadonlyRequestCookiesError()
}
delete() {
throw readonlyCookiesError()
throw new ReadonlyRequestCookiesError()
}
set() {
throw readonlyCookiesError()
throw new ReadonlyRequestCookiesError()
}
}

Expand Down Expand Up @@ -1662,7 +1656,7 @@ export async function renderToHTMLOrFlight(
)

let cachedHeadersInstance: ReadonlyHeaders | undefined
let cachedCookiesInstance: ReadonlyNextCookies | undefined
let cachedCookiesInstance: ReadonlyRequestCookies | undefined

const requestStore = {
get headers() {
Expand All @@ -1675,7 +1669,7 @@ export async function renderToHTMLOrFlight(
},
get cookies() {
if (!cachedCookiesInstance) {
cachedCookiesInstance = new ReadonlyNextCookies({
cachedCookiesInstance = new ReadonlyRequestCookies({
headers: {
get: (key) => {
if (key !== 'cookie') {
Expand Down
143 changes: 0 additions & 143 deletions packages/next/server/web/spec-extension/cookies.ts

This file was deleted.

16 changes: 16 additions & 0 deletions packages/next/server/web/spec-extension/cookies/cached.ts
@@ -0,0 +1,16 @@
/**
* A simple caching behavior.
* We cache the result based on the key `K`
* which uses referential equality, to avoid re-computing
* the result for the same key.
*/
export function cached<K, V>(generate: (key: K) => V) {
let cache: { key: K; value: V } | undefined = undefined
return (key: K) => {
if (cache?.key !== key) {
cache = { key, value: generate(key) }
}

return cache.value
}
}
4 changes: 4 additions & 0 deletions packages/next/server/web/spec-extension/cookies/index.ts
@@ -0,0 +1,4 @@
// TODO: use `@edge-runtime/cookies`
export type { CookieListItem, RequestCookie, ResponseCookie } from './types'
export { RequestCookies } from './request-cookies'
export { ResponseCookies } from './response-cookies'

0 comments on commit 4d20beb

Please sign in to comment.