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

Add upgrade guide for Middleware. #37382

Merged
merged 23 commits into from
Jun 16, 2022
Merged
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 292 additions & 0 deletions errors/middleware-upgrade-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
# Middleware Upgrade Guide

As we work on improving Middleware for General Availability (GA), we've made some changes to the Middleware APIs (and how you define Middleware in your application) based on your feedback.
leerob marked this conversation as resolved.
Show resolved Hide resolved

This upgrade guide will help you understand the changes and how to migrate your existing Middleware to the new API. The guide is for Next.js customers who:
leerob marked this conversation as resolved.
Show resolved Hide resolved
leerob marked this conversation as resolved.
Show resolved Hide resolved

- Currently use the beta Next.js Middleware features
- Choose to upgrade to the next stable version of Next.js
leerob marked this conversation as resolved.
Show resolved Hide resolved

## Using Next.js Middleware on Vercel

If you're using Next.js on Vercel, your existing deploys using Middleware will continue to work, and you can continue to deploy your site using Middleware. When you upgrade your site to the next stable version of Next.js (`v12.2`), you will need to follow this upgrade guide to update your Middleware.

## Breaking changes

1. [No Nested Middleware](#no-nested-middleware)
2. [No Response Body](#no-response-body)
3. [Cookies API Revamped](#cookies-api-revamped)
4. [No More Page Match Data](#no-more-page-match-data)
5. [Executing Middleware on Internal Next.js Requests](#executing-middleware-on-internal-next.js-requests)
leerob marked this conversation as resolved.
Show resolved Hide resolved
leerob marked this conversation as resolved.
Show resolved Hide resolved

## No Nested Middleware

### Summary of changes

- Define a single Middleware file at the root of your project
- No need to prefix the file with an underscore
- A custom matcher can be used to define matching routes using an exported config object

### Explanation

Previously, you could create a `_middleware.js` file under the `pages` directory at any level. Middleware execution was based on the file path where it was created. Beta customers found this route matching confusing. For example:
leerob marked this conversation as resolved.
Show resolved Hide resolved
leerob marked this conversation as resolved.
Show resolved Hide resolved

- Middleware in `pages/dashboard/_middleware.ts`
- Middleware in `pages/dashboard/users/_middleware.ts`
- A request to `/dashboard/users/*` **would match both.**

Based on customer feedback, we have replaced this API with a single root Middleware.

### How to upgrade

You should declare **one single Middleware file** in your application, which should be located at the root of the project directory (**not** inside of the `pages` directory), and named without an `_` prefix. Your Middleware file can still have either a `.ts` or `.js` extension.
leerob marked this conversation as resolved.
Show resolved Hide resolved

Middleware will be invoked for **every route in the app**, and a custom matcher can be used to define matching filters. The following is an example for a Middleware that triggers for `/about/*`, the custom matcher is defined in an exported config object:
leerob marked this conversation as resolved.
Show resolved Hide resolved
leerob marked this conversation as resolved.
Show resolved Hide resolved

```typescript
// middleware.ts
import type { NextRequest } from 'next/server'
leerob marked this conversation as resolved.
Show resolved Hide resolved

export function middleware(request: NextRequest) {
return NextResponse.rewrite(new URL('/about-2', request.url))
leerob marked this conversation as resolved.
Show resolved Hide resolved
}
// config with custom matcher
leerob marked this conversation as resolved.
Show resolved Hide resolved
export const config = {
leerob marked this conversation as resolved.
Show resolved Hide resolved
leerob marked this conversation as resolved.
Show resolved Hide resolved
matcher: '/about/:path*',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arrays are now supported

}
```

leerob marked this conversation as resolved.
Show resolved Hide resolved
You can also use a conditional statement to only run the Middleware when it matches the `about/*` path:
leerob marked this conversation as resolved.
Show resolved Hide resolved
leerob marked this conversation as resolved.
Show resolved Hide resolved

```typescript
// <root>/middleware.js
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/about')) {
// This logic is only applied to /about
}

if (request.nextUrl.pathname.startsWith('/dashboard')) {
// This logic is only applied to /dashboard
}
}
```

## No Response Body

### Summary of changes

- Middleware can no longer respond with a body
- If your Middleware _does_ respond with a body, a runtime error will be thrown
- Migrate to using `rewrites`/`redirects` to pages that handle authorization
leerob marked this conversation as resolved.
Show resolved Hide resolved

### Explanation

Beta customers had explored using Middleware to handle authorization for their application. However, to ensure both the HTML and data payload (JSON file) are protected, we recommend checking authorization at the Page level.
leerob marked this conversation as resolved.
Show resolved Hide resolved

To help ensure security, we are removing the ability to send response bodies in Middleware. This ensures that Middleware is only used to `rewrite`, `redirect`, or modify the incoming request (e.g. [setting cookies](#cookies-api-revamped)).

The following patterns will no longer work:

```js
new Response('a text value')
new Response(streamOrBuffer)
new Response(JSON.stringify(obj), { headers: 'application/json' })
leerob marked this conversation as resolved.
Show resolved Hide resolved
leerob marked this conversation as resolved.
Show resolved Hide resolved
```

### How to upgrade

For cases where Middleware is used to respond (such as authorization), you should migrate to use `rewrites`/`redirects` to Pages that show an authorization error, login forms, or to an API Route.
leerob marked this conversation as resolved.
Show resolved Hide resolved
leerob marked this conversation as resolved.
Show resolved Hide resolved
leerob marked this conversation as resolved.
Show resolved Hide resolved

leerob marked this conversation as resolved.
Show resolved Hide resolved
```typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { credentialsValid } from './lib/auth'
leerob marked this conversation as resolved.
Show resolved Hide resolved

export function middleware(request: NextRequest) {
const basicAuth = request.headers.get('authorization')

if (basicAuth) {
const auth = basicAuth.split(' ')[1]
const [user, pwd] = atob(auth).split(':')
// Example function to validate auth
if (credentialsValid(user, pwd)) {
return NextResponse.next()
}
}
leerob marked this conversation as resolved.
Show resolved Hide resolved

const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('from', request.nextUrl.pathname)

return NextResponse.redirect(loginUrl)
}
```

## Cookies API Revamped

### Summary of changes

**Added**
leerob marked this conversation as resolved.
Show resolved Hide resolved

- `cookie.delete`
- `cookie.set`
- `cookie.getAll`
leerob marked this conversation as resolved.
Show resolved Hide resolved

**Removed:**

- `cookie`
- `cookies`
- `clearCookie`

### Explanation

Based on beta feedback, we are changing the Cookies API in `NextRequest` and `NextResponse` to align more to a `get`/`set` model.

### How to upgrade

`NextResponse` now has a `cookies` instance with:

- `cookie.delete`
- `cookie.set`
- `cookie.getAll`
leerob marked this conversation as resolved.
Show resolved Hide resolved

#### Before

```javascript
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
// create an instance of the class to access the public methods. This uses `next()`,
// you could use `redirect()` or `rewrite()` as well
let response = NextResponse.next()
// get the cookies from the request
let cookieFromRequest = request.cookies['my-cookie']
leerob marked this conversation as resolved.
Show resolved Hide resolved
leerob marked this conversation as resolved.
Show resolved Hide resolved
// set the `cookie`
response.cookie('hello', 'world')
// set the `cookie` with options
const cookieWithOptions = response.cookie('hello', 'world', {
path: '/',
maxAge: 1000 * 60 * 60 * 24 * 7,
httpOnly: true,
sameSite: 'strict',
domain: 'example.com',
})
// clear the `cookie`
response.clearCookie('hello')

return response
}
```

#### After
leerob marked this conversation as resolved.
Show resolved Hide resolved

```typescript
// middleware.ts
export function middleware() {
const response = new NextResponse()

// set a cookie
response.cookies.set('vercel', 'fast')

// set another cookie with options
response.cookies.set('nextjs', 'awesome', { path: '/test' })

// get all the details of a cookie
const [value, options] = response.cookies.getWithOptions('vercel')
leerob marked this conversation as resolved.
Show resolved Hide resolved
console.log(value) // => 'fast'
console.log(options) // => { Path: '/test' }

// delete a cookie means mark it as expired
leerob marked this conversation as resolved.
Show resolved Hide resolved
response.cookies.delete('vercel')

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

leerob marked this conversation as resolved.
Show resolved Hide resolved
## No More Page Match Data

### Summary of changes

- Use [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) to check if a Middleware is being invoked for a certain page match
leerob marked this conversation as resolved.
Show resolved Hide resolved

### Explanation

Currently, Middleware estimates whether you are serving an asset of a Page based on the Next.js routes manifest (internal configuration). This value is surfaced through `request.page`.

To make this more accurate, we are recommending to use the web standard `URLPattern` API.
leerob marked this conversation as resolved.
Show resolved Hide resolved

### How to upgrade

Use [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) to check if a Middleware is being invoked for a certain page match.

#### Before

```typescript
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
leerob marked this conversation as resolved.
Show resolved Hide resolved
leerob marked this conversation as resolved.
Show resolved Hide resolved

export function middleware(request: NextRequest) {
const { params } = event.request.page
const { locale, slug } = params

if (locale && slug) {
const { search, protocol, host } = request.nextUrl
const url = new URL(`${protocol}//${locale}.${host}/${slug}${search}`)
return NextResponse.redirect(url)
}
}
```

#### After

```typescript
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
leerob marked this conversation as resolved.
Show resolved Hide resolved
leerob marked this conversation as resolved.
Show resolved Hide resolved

const PATTERNS = [
[
new URLPattern({ pathname: '/:locale/:slug' }),
({ pathname }) => pathname.groups,
],
]

const params = (url) => {
const input = url.split('?')[0]
let result = {}

for (const [pattern, handler] of PATTERNS) {
const patternResult = pattern.exec(input)
if (patternResult !== null && 'pathname' in patternResult) {
result = handler(patternResult)
break
}
}
return result
}

export function middleware(request: NextRequest) {
const { locale, slug } = params(request.url)

if (locale && slug) {
const { search, protocol, host } = request.nextUrl
const url = new URL(`${protocol}//${locale}.${host}/${slug}${search}`)
return NextResponse.redirect(url)
}
}
```

## Executing Middleware on Internal Next.js Requests

### Summary of changes

- Middleware will be executed for _all_ requests, including `_next`

### Explanation

Currently, we do not execute Middleware for `_next` requests, due the authorization use cases.
leerob marked this conversation as resolved.
Show resolved Hide resolved

For cases where Middleware is used for authorization, you should migrate to use `rewrites`/`redirects` to Pages that show an authorization error, login forms, or to an API Route.