Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING CHANGE: feat(edge): split NextCookies to RequestCookies and ResponseCookies #41526

Merged
merged 74 commits into from Oct 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
918a55a
feat(edge): add `getAll`, better types for `NextCookies`
balazsorban44 Oct 18, 2022
d638c7a
Merge branch 'canary' into feat/extend-nextcookies
balazsorban44 Oct 18, 2022
1d27f0c
Merge branch 'canary' into feat/extend-nextcookies
balazsorban44 Oct 19, 2022
e965fc0
feat(edge): flatten `getWithOptions`
balazsorban44 Oct 19, 2022
990b514
add `name`
balazsorban44 Oct 19, 2022
6f127fa
add `getAll` to `ReadOnlyNextCookies`
balazsorban44 Oct 19, 2022
52e76cd
update tests/docs
balazsorban44 Oct 19, 2022
acc8fa8
do not destructure
balazsorban44 Oct 19, 2022
3f6c20d
revert
balazsorban44 Oct 19, 2022
03b31b0
fix tests
balazsorban44 Oct 19, 2022
a82325c
align `next-codemod` structure with other packages
balazsorban44 Oct 21, 2022
72edcfa
strongly type `next/jest` config
balazsorban44 Oct 21, 2022
c928ef2
simplify test running
balazsorban44 Oct 21, 2022
08e1c47
revert testfixture changes
balazsorban44 Oct 21, 2022
d7e9805
revert textfitures
balazsorban44 Oct 21, 2022
33948f6
prettierignore
balazsorban44 Oct 21, 2022
f905ca2
fix failing tests
balazsorban44 Oct 21, 2022
d42c01e
wip
balazsorban44 Oct 22, 2022
98187ac
alias `NextCookies` with `@edge-runtime/cookies`#ResponseCookies
balazsorban44 Oct 22, 2022
6a9202f
Merge branch 'canary' into feat/extend-nextcookies
balazsorban44 Oct 22, 2022
c9835d4
fix merge
balazsorban44 Oct 22, 2022
697cb9b
remove unused type export
balazsorban44 Oct 22, 2022
608b960
fix build
balazsorban44 Oct 22, 2022
a1f740a
revert
balazsorban44 Oct 22, 2022
46f24f5
revert
balazsorban44 Oct 22, 2022
6d3d1e3
fix formatting
balazsorban44 Oct 22, 2022
d2d6617
rename
balazsorban44 Oct 22, 2022
21681f9
fix tests
balazsorban44 Oct 22, 2022
5a0195f
use `CookieStore` for `NextCookies`
balazsorban44 Oct 22, 2022
3db6664
revert refactor
balazsorban44 Oct 23, 2022
8c02ab3
`RequestCookies` & `ResponseCookies` instead of `NextCookies`
balazsorban44 Oct 23, 2022
232c32c
rename test file
balazsorban44 Oct 23, 2022
f6b8449
revert jest
balazsorban44 Oct 23, 2022
b3a1c27
document `RequestCookies` and `ResponseCookies`
balazsorban44 Oct 23, 2022
3deeca2
preocmpile cookies
balazsorban44 Oct 24, 2022
9c9ada6
Merge branch 'canary' into feat/extend-nextcookies
balazsorban44 Oct 24, 2022
32b2dff
lock file
balazsorban44 Oct 24, 2022
bda1b39
fix typos
balazsorban44 Oct 24, 2022
d0e3d98
delete unnecessary tests
balazsorban44 Oct 24, 2022
5a4a18d
revert codemod
balazsorban44 Oct 24, 2022
f905f9c
Merge branch 'canary' into feat/extend-nextcookies
balazsorban44 Oct 24, 2022
0f9fc7e
fix tests
balazsorban44 Oct 24, 2022
e23bdff
Merge branch 'feat/extend-nextcookies' of github.com:vercel/next.js i…
balazsorban44 Oct 24, 2022
a38627d
remove `ResponseCookies#clear`
balazsorban44 Oct 24, 2022
5492eec
fix test
balazsorban44 Oct 24, 2022
c390ef5
fix test
balazsorban44 Oct 24, 2022
625df69
Merge branch 'canary' into feat/extend-nextcookies
balazsorban44 Oct 24, 2022
c96fc9a
fix remaining tests
balazsorban44 Oct 24, 2022
f19595c
do not prebundle `@edge-runtime/cookies`
balazsorban44 Oct 24, 2022
907917e
Apply suggestions from code review
balazsorban44 Oct 24, 2022
4010ddd
Merge branch 'canary' into feat/extend-nextcookies
balazsorban44 Oct 24, 2022
ab15dbc
move log in middleware docs
balazsorban44 Oct 24, 2022
722f77c
Merge branch 'feat/extend-nextcookies' of github.com:vercel/next.js i…
balazsorban44 Oct 24, 2022
0437582
add link to spec
balazsorban44 Oct 24, 2022
075afd5
fix test and types
balazsorban44 Oct 24, 2022
e857d8d
Apply suggestions from code review
balazsorban44 Oct 24, 2022
72b8ec7
upgrae @edge-runtime/cookies
balazsorban44 Oct 24, 2022
267774a
Merge branch 'feat/extend-nextcookies' of github.com:vercel/next.js i…
balazsorban44 Oct 24, 2022
a96d299
Merge branch 'canary' into feat/extend-nextcookies
balazsorban44 Oct 24, 2022
e62cf63
prebundle `@edge-runtime/cookies`
balazsorban44 Oct 24, 2022
9bcf858
Merge branch 'feat/extend-nextcookies' of github.com:vercel/next.js i…
balazsorban44 Oct 24, 2022
f2ba008
support .has
balazsorban44 Oct 25, 2022
6fd2bfe
bump `@edge-runtime/cookies`
balazsorban44 Oct 25, 2022
c8c64e9
remove .only
balazsorban44 Oct 25, 2022
711b89f
fix test
balazsorban44 Oct 25, 2022
2eede08
use `@edge-runtime/cookies` code directly for now
balazsorban44 Oct 25, 2022
eaa1b22
fix lock file
balazsorban44 Oct 25, 2022
54be68a
fix ts
balazsorban44 Oct 25, 2022
95a4ec5
fix type
balazsorban44 Oct 25, 2022
f7cc401
Merge branch 'canary' into feat/extend-nextcookies
balazsorban44 Oct 25, 2022
199f28f
Merge branch 'canary' into feat/extend-nextcookies
balazsorban44 Oct 26, 2022
d7d7fdc
Merge branch 'canary' into feat/extend-nextcookies
balazsorban44 Oct 26, 2022
afc4dd5
Merge branch 'canary' into feat/extend-nextcookies
ijjk Oct 27, 2022
1104720
Merge branch 'canary' into feat/extend-nextcookies
kodiakhq[bot] Oct 27, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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'