Skip to content

Commit

Permalink
New Middleware API signature (#30282)
Browse files Browse the repository at this point in the history
Co-authored-by: Steven <steven@ceriously.com>
  • Loading branch information
javivelasco and styfle committed Oct 25, 2021
1 parent 4782cac commit 0910e8b
Show file tree
Hide file tree
Showing 14 changed files with 190 additions and 114 deletions.
3 changes: 0 additions & 3 deletions packages/next/server/dev/next-dev-server.ts
Expand Up @@ -475,9 +475,6 @@ export default class DevServer extends Server {
}): Promise<FetchEventResult | null> {
try {
const result = await super.runMiddleware(params)
result?.promise.catch((error) =>
this.logErrorWithOriginalStack(error, 'unhandledRejection', 'client')
)
result?.waitUntil.catch((error) =>
this.logErrorWithOriginalStack(error, 'unhandledRejection', 'client')
)
Expand Down
16 changes: 12 additions & 4 deletions packages/next/server/next-server.ts
Expand Up @@ -102,6 +102,7 @@ import isError from '../lib/is-error'
import { getMiddlewareInfo } from './require'
import { parseUrl as simpleParseUrl } from '../shared/lib/router/utils/parse-url'
import { MIDDLEWARE_ROUTE } from '../lib/constants'
import { NextResponse } from './web/spec-extension/response'
import { run } from './web/sandbox'
import type { FetchEventResult } from './web/types'
import type { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin'
Expand Down Expand Up @@ -621,6 +622,9 @@ export default class Server {
}
}

const subreq = params.request.headers[`x-middleware-subrequest`]
const subrequests = typeof subreq === 'string' ? subreq.split(':') : []

let result: FetchEventResult | null = null

for (const middleware of this.middleware || []) {
Expand All @@ -639,6 +643,14 @@ export default class Server {
serverless: this._isLikeServerless,
})

if (subrequests.includes(middlewareInfo.name)) {
result = {
response: NextResponse.next(),
waitUntil: Promise.resolve(),
}
continue
}

result = await run({
name: middlewareInfo.name,
paths: middlewareInfo.paths,
Expand All @@ -656,10 +668,6 @@ export default class Server {
})

if (!this.renderOpts.dev) {
result.promise.catch((error) => {
console.error(`Uncaught: middleware error after responding`, error)
})

result.waitUntil.catch((error) => {
console.error(`Uncaught: middleware waitUntil errored`, error)
})
Expand Down
49 changes: 32 additions & 17 deletions packages/next/server/web/adapter.ts
@@ -1,36 +1,51 @@
import type { RequestData, FetchEventResult } from './types'

import { DeprecationError } from './error'
import { fromNodeHeaders } from './utils'
import { NextFetchEvent } from './spec-extension/fetch-event'
import { NextRequest } from './spec-extension/request'
import { NextRequest, RequestInit } from './spec-extension/request'
import { NextResponse } from './spec-extension/response'
import { waitUntilSymbol, responseSymbol } from './spec-compliant/fetch-event'
import { waitUntilSymbol } from './spec-compliant/fetch-event'

export async function adapter(params: {
handler: (event: NextFetchEvent) => void | Promise<void>
handler: (request: NextRequest, event: NextFetchEvent) => Promise<Response>
request: RequestData
}): Promise<FetchEventResult> {
const url = params.request.url.startsWith('/')
? `https://${params.request.headers.host}${params.request.url}`
: params.request.url

const event = new NextFetchEvent(
new NextRequest(url, {
geo: params.request.geo,
headers: fromNodeHeaders(params.request.headers),
ip: params.request.ip,
method: params.request.method,
nextConfig: params.request.nextConfig,
page: params.request.page,
})
)
const request = new NextRequestHint(url, {
geo: params.request.geo,
headers: fromNodeHeaders(params.request.headers),
ip: params.request.ip,
method: params.request.method,
nextConfig: params.request.nextConfig,
page: params.request.page,
})

const handled = params.handler(event)
const original = await event[responseSymbol]
const event = new NextFetchEvent(request)
const original = await params.handler(request, event)

return {
promise: Promise.resolve(handled),
response: original || NextResponse.next(),
waitUntil: Promise.all(event[waitUntilSymbol]),
}
}

class NextRequestHint extends NextRequest {
constructor(input: Request | string, init: RequestInit = {}) {
super(input, init)
}

get request() {
throw new DeprecationError()
}

respondWith() {
throw new DeprecationError()
}

waitUntil() {
throw new DeprecationError()
}
}
10 changes: 10 additions & 0 deletions packages/next/server/web/error.ts
@@ -0,0 +1,10 @@
export class DeprecationError extends Error {
constructor() {
super(`Middleware now accepts an async API directly with the form:
export function middleware(request, event) {
return new Response("Hello " + request.url)
}
`)
}
}
43 changes: 41 additions & 2 deletions packages/next/server/web/sandbox/sandbox.ts
@@ -1,4 +1,4 @@
import type { RequestData, FetchEventResult } from '../types'
import type { RequestData, FetchEventResult, NodeHeaders } from '../types'
import { Blob, File, FormData } from 'next/dist/compiled/formdata-node'
import { dirname } from 'path'
import { ReadableStream } from 'next/dist/compiled/web-streams-polyfill'
Expand Down Expand Up @@ -57,7 +57,20 @@ export async function run(params: {
},
Crypto: polyfills.Crypto,
crypto: new polyfills.Crypto(),
fetch,
fetch: (input: RequestInfo, init: RequestInit = {}) => {
const url = getFetchURL(input, params.request.headers)
init.headers = getFetchHeaders(params.name, init)
if (isRequestLike(input)) {
return fetch(url, {
...init,
headers: {
...Object.fromEntries(input.headers),
...Object.fromEntries(init.headers),
},
})
}
return fetch(url, init)
},
File,
FormData,
process: { env: { ...process.env } },
Expand Down Expand Up @@ -168,3 +181,29 @@ function sandboxRequire(referrer: string, specifier: string) {
module.loaded = true
return module.exports
}

function getFetchHeaders(middleware: string, init: RequestInit) {
const headers = new Headers(init.headers ?? {})
const prevsub = headers.get(`x-middleware-subrequest`) || ''
const value = prevsub.split(':').concat(middleware).join(':')
headers.set(`x-middleware-subrequest`, value)
headers.set(`user-agent`, `Next.js Middleware`)
return headers
}

function getFetchURL(input: RequestInfo, headers: NodeHeaders = {}): string {
const initurl = isRequestLike(input) ? input.url : input
if (initurl.startsWith('/')) {
const host = headers.host?.toString()
const localhost =
host === '127.0.0.1' ||
host === 'localhost' ||
host?.startsWith('localhost:')
return `${localhost ? 'http' : 'https'}://${host}${initurl}`
}
return initurl
}

function isRequestLike(obj: unknown): obj is Request {
return Boolean(obj && typeof obj === 'object' && 'url' in obj)
}
5 changes: 5 additions & 0 deletions packages/next/server/web/spec-extension/fetch-event.ts
@@ -1,3 +1,4 @@
import { DeprecationError } from '../error'
import { FetchEvent } from '../spec-compliant/fetch-event'
import { NextRequest } from './request'

Expand All @@ -7,4 +8,8 @@ export class NextFetchEvent extends FetchEvent {
super(request)
this.request = request
}

respondWith() {
throw new DeprecationError()
}
}
2 changes: 1 addition & 1 deletion packages/next/server/web/spec-extension/request.ts
Expand Up @@ -91,7 +91,7 @@ export class NextRequest extends Request {
}
}

interface RequestInit extends globalThis.RequestInit {
export interface RequestInit extends globalThis.RequestInit {
geo?: {
city?: string
country?: string
Expand Down
1 change: 0 additions & 1 deletion packages/next/server/web/types.ts
Expand Up @@ -26,7 +26,6 @@ export interface RequestData {
}

export interface FetchEventResult {
promise: Promise<any>
response: Response
waitUntil: Promise<any>
}
8 changes: 4 additions & 4 deletions test/integration/middleware-base-path/pages/_middleware.js
@@ -1,14 +1,14 @@
import { NextResponse } from 'next/server'

export function middleware(event) {
const url = event.request.nextUrl
export async function middleware(request) {
const url = request.nextUrl
if (url.pathname === '/redirect-with-basepath' && !url.basePath) {
url.basePath = '/root'
event.respondWith(NextResponse.redirect(url))
return NextResponse.redirect(url)
}

if (url.pathname === '/redirect-with-basepath') {
url.pathname = '/about'
event.respondWith(NextResponse.rewrite(url))
return NextResponse.rewrite(url)
}
}
24 changes: 11 additions & 13 deletions test/integration/middleware-core/pages/interface/_middleware.js
@@ -1,14 +1,12 @@
export function middleware(event) {
event.respondWith(
new Response(null, {
headers: {
'req-url-basepath': event.request.nextUrl.basePath,
'req-url-pathname': event.request.nextUrl.pathname,
'req-url-params': JSON.stringify(event.request.page.params),
'req-url-page': event.request.page.name,
'req-url-query': event.request.nextUrl.searchParams.get('foo'),
'req-url-locale': event.request.nextUrl.locale,
},
})
)
export function middleware(request) {
return new Response(null, {
headers: {
'req-url-basepath': request.nextUrl.basePath,
'req-url-pathname': request.nextUrl.pathname,
'req-url-params': JSON.stringify(request.page.params),
'req-url-page': request.page.name,
'req-url-query': request.nextUrl.searchParams.get('foo'),
'req-url-locale': request.nextUrl.locale,
},
})
}
@@ -1,9 +1,5 @@
export function middleware(event) {
event.respondWith(handleRequest(event))
}

async function handleRequest(event) {
const url = event.request.nextUrl
export async function middleware(request) {
const url = request.nextUrl

if (url.searchParams.get('foo') === 'bar') {
url.pathname = '/redirects/new-home'
Expand Down

0 comments on commit 0910e8b

Please sign in to comment.