2022-01-24 22:38:54 +01:00
import './node-polyfill-fetch'
2022-04-07 16:26:30 +02:00
import './node-polyfill-web-streams'
2022-07-22 20:42:35 +02:00
import type { TLSSocket } from 'tls'
2022-05-19 17:46:21 +02:00
import type { Route } from './router'
2022-06-24 20:50:49 +02:00
import {
CacheFs ,
DecodeError ,
PageNotFoundError ,
MiddlewareNotFoundError ,
} from '../shared/lib/utils'
2022-01-20 22:25:44 +01:00
import type { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin'
import type RenderResult from './render-result'
2022-04-06 16:35:52 +02:00
import type { FetchEventResult } from './web/types'
2022-01-21 16:31:47 +01:00
import type { PrerenderManifest } from '../build'
2022-08-16 16:47:27 +02:00
import type { CustomRoutes , Rewrite } from '../lib/load-custom-routes'
2022-02-11 20:56:25 +01:00
import type { BaseNextRequest , BaseNextResponse } from './base-http'
2022-02-18 23:39:28 +01:00
import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin'
2022-02-24 13:43:22 +01:00
import type { PayloadOptions } from './send-payload'
2022-04-07 16:26:30 +02:00
import type { NextParsedUrlQuery , NextUrlWithParsedQuery } from './request-meta'
2022-06-03 18:35:44 +02:00
import type {
Params ,
RouteMatch ,
} from '../shared/lib/router/utils/route-matcher'
2022-08-16 16:47:27 +02:00
import type { NextConfig } from './config-shared'
import type { DynamicRoutes , PageChecker } from './router'
2021-12-17 23:56:26 +01:00
2021-12-07 02:14:55 +01:00
import fs from 'fs'
2022-01-19 22:54:04 +01:00
import { join , relative , resolve , sep } from 'path'
2022-01-14 22:01:35 +01:00
import { IncomingMessage , ServerResponse } from 'http'
2022-04-07 16:26:30 +02:00
import { addRequestMeta , getRequestMeta } from './request-meta'
2022-08-16 16:47:27 +02:00
import { isDynamicRoute } from '../shared/lib/router/utils'
2022-01-19 13:36:06 +01:00
import {
PAGES_MANIFEST ,
BUILD_ID_FILE ,
MIDDLEWARE_MANIFEST ,
2022-01-19 22:54:04 +01:00
CLIENT_STATIC_FILES_PATH ,
CLIENT_STATIC_FILES_RUNTIME ,
2022-01-21 16:31:47 +01:00
PRERENDER_MANIFEST ,
ROUTES_MANIFEST ,
2022-06-26 23:01:26 +02:00
FLIGHT_MANIFEST ,
2022-01-21 17:24:57 +01:00
CLIENT_PUBLIC_FILES_PATH ,
2022-05-25 11:46:26 +02:00
APP_PATHS_MANIFEST ,
2022-08-12 15:01:19 +02:00
FLIGHT_SERVER_CSS_MANIFEST ,
2022-08-15 20:58:49 +02:00
SERVERLESS_DIRECTORY ,
SERVER_DIRECTORY ,
2022-01-19 13:36:06 +01:00
} from '../shared/lib/constants'
2021-12-07 02:14:55 +01:00
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
2022-01-14 22:01:35 +01:00
import { format as formatUrl , UrlWithParsedQuery } from 'url'
import compression from 'next/dist/compiled/compression'
2022-02-18 20:43:43 +01:00
import HttpProxy from 'next/dist/compiled/http-proxy'
2022-04-27 11:50:29 +02:00
import { getPathMatch } from '../shared/lib/router/utils/path-match'
2022-08-16 16:47:27 +02:00
import { createHeaderRoute , createRedirectRoute } from './server-route-utils'
import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-asset-path'
2022-01-19 13:36:06 +01:00
import { run } from './web/sandbox'
2022-08-16 16:47:27 +02:00
import { detectDomainLocale } from '../shared/lib/i18n/detect-domain-locale'
2022-01-14 22:01:35 +01:00
2022-02-11 20:56:25 +01:00
import { NodeNextRequest , NodeNextResponse } from './base-http/node'
2022-02-24 13:43:22 +01:00
import { sendRenderResult } from './send-payload'
2022-02-09 00:46:59 +01:00
import { getExtension , serveStatic } from './serve-static'
2022-01-14 22:01:35 +01:00
import { ParsedUrlQuery } from 'querystring'
2022-02-11 20:56:25 +01:00
import { apiResolver } from './api-utils/node'
2022-01-14 22:01:35 +01:00
import { RenderOpts , renderToHTML } from './render'
2022-07-26 12:41:51 +02:00
import { renderToHTMLOrFlight as appRenderToHTMLOrFlight } from './app-render'
2022-03-03 20:36:53 +01:00
import { ParsedUrl , parseUrl } from '../shared/lib/router/utils/parse-url'
2022-08-10 19:00:30 +02:00
import { parse as nodeParseUrl } from 'url'
2022-01-19 13:36:06 +01:00
import * as Log from '../build/output/log'
2022-08-09 03:27:42 +02:00
import loadRequireHook from '../build/webpack/require-hook'
2022-01-14 22:01:35 +01:00
import BaseServer , {
2022-01-20 22:25:44 +01:00
Options ,
2022-01-14 22:01:35 +01:00
FindComponentsResult ,
prepareServerlessUrl ,
2022-05-19 17:46:21 +02:00
RoutingItem ,
2022-08-16 16:47:27 +02:00
NoFallbackError ,
2022-01-14 22:01:35 +01:00
} from './base-server'
2022-06-06 20:35:26 +02:00
import { getPagePath , requireFontManifest } from './require'
2022-05-19 17:46:21 +02:00
import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path'
2022-04-30 13:19:27 +02:00
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
2022-01-12 19:12:36 +01:00
import { loadComponents } from './load-components'
2022-01-19 13:36:06 +01:00
import isError , { getProperError } from '../lib/is-error'
2022-01-14 22:01:35 +01:00
import { FontManifest } from './font-utils'
2022-01-19 13:36:06 +01:00
import { toNodeHeaders } from './web/utils'
import { relativizeURL } from '../shared/lib/router/utils/relativize-url'
import { prepareDestination } from '../shared/lib/router/utils/prepare-destination'
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
2022-05-19 17:46:21 +02:00
import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher'
2022-01-20 22:25:44 +01:00
import { loadEnvConfig } from '@next/env'
2022-08-16 16:47:27 +02:00
import { getCustomRoute , stringifyQuery } from './server-route-utils'
2022-01-27 23:06:39 +01:00
import { urlQueryToSearchParams } from '../shared/lib/router/utils/querystring'
2022-05-27 20:29:04 +02:00
import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash'
2022-05-31 22:11:12 +02:00
import { getNextPathnameInfo } from '../shared/lib/router/utils/get-next-pathname-info'
2022-07-22 20:42:35 +02:00
import { bodyStreamToNodeStream , getClonableBody } from './body-streams'
2022-06-16 18:22:35 +02:00
import { checkIsManualRevalidate } from './api-utils'
2022-08-15 20:58:49 +02:00
import { shouldUseReactRoot , isTargetLikeServerless } from './utils'
2022-08-12 17:25:47 +02:00
import ResponseCache from './response-cache'
import { IncrementalCache } from './lib/incremental-cache'
2022-08-14 23:51:11 +02:00
import { getSortedRoutes } from '../shared/lib/router/utils/sorted-routes'
2021-12-17 23:56:26 +01:00
2022-06-09 15:43:38 +02:00
if ( shouldUseReactRoot ) {
; ( process . env as any ) . __NEXT_REACT_ROOT = 'true'
}
2022-08-09 03:27:42 +02:00
loadRequireHook ( )
2021-12-05 22:53:11 +01:00
export * from './base-server'
2021-11-28 17:48:43 +01:00
2022-01-14 22:01:35 +01:00
type ExpressMiddleware = (
req : IncomingMessage ,
res : ServerResponse ,
next : ( err? : Error ) = > void
) = > void
export interface NodeRequestHandler {
(
req : IncomingMessage | BaseNextRequest ,
res : ServerResponse | BaseNextResponse ,
parsedUrl? : NextUrlWithParsedQuery | undefined
) : Promise < void >
}
2022-08-15 16:29:51 +02:00
const MiddlewareMatcherCache = new WeakMap <
MiddlewareManifest [ 'middleware' ] [ string ] ,
RouteMatch
> ( )
function getMiddlewareMatcher (
info : MiddlewareManifest [ 'middleware' ] [ string ]
) : RouteMatch {
const stored = MiddlewareMatcherCache . get ( info )
if ( stored ) {
return stored
}
if ( typeof info . regexp !== 'string' || ! info . regexp ) {
throw new Error (
` Invariant: invalid regexp for middleware ${ JSON . stringify ( info ) } `
)
}
const matcher = getRouteMatcher ( { re : new RegExp ( info . regexp ) , groups : { } } )
MiddlewareMatcherCache . set ( info , matcher )
return matcher
}
2021-12-07 02:14:55 +01:00
export default class NextNodeServer extends BaseServer {
2022-02-09 19:37:55 +01:00
private imageResponseCache? : ResponseCache
2022-02-09 00:46:59 +01:00
2022-01-20 22:25:44 +01:00
constructor ( options : Options ) {
2022-01-26 07:22:11 +01:00
// Initialize super class
2022-01-20 22:25:44 +01:00
super ( options )
2022-01-26 07:22:11 +01:00
2022-01-20 22:25:44 +01:00
/ * *
* This sets environment variable to be used at the time of SSR by head . tsx .
* Using this from process . env allows targeting both serverless and SSR by calling
2022-02-15 02:36:51 +01:00
* ` process.env.__NEXT_OPTIMIZE_CSS ` .
2022-01-20 22:25:44 +01:00
* /
if ( this . renderOpts . optimizeFonts ) {
process . env . __NEXT_OPTIMIZE_FONTS = JSON . stringify ( true )
}
if ( this . renderOpts . optimizeCss ) {
process . env . __NEXT_OPTIMIZE_CSS = JSON . stringify ( true )
}
2022-03-11 23:26:46 +01:00
if ( this . renderOpts . nextScriptWorkers ) {
process . env . __NEXT_SCRIPT_WORKERS = JSON . stringify ( true )
}
2022-02-09 00:46:59 +01:00
2022-02-09 19:37:55 +01:00
if ( ! this . minimalMode ) {
const { ImageOptimizerCache } =
require ( './image-optimizer' ) as typeof import ( './image-optimizer' )
this . imageResponseCache = new ResponseCache (
new ImageOptimizerCache ( {
distDir : this.distDir ,
nextConfig : this.nextConfig ,
2022-03-03 00:06:54 +01:00
} ) ,
this . minimalMode
2022-02-09 19:37:55 +01:00
)
}
2022-02-10 03:53:04 +01:00
2022-05-05 18:11:17 +02:00
if ( ! options . dev ) {
2022-02-10 03:53:04 +01:00
// pre-warm _document and _app as these will be
// needed for most requests
loadComponents ( this . distDir , '/_document' , this . _isLikeServerless ) . catch (
( ) = > { }
)
loadComponents ( this . distDir , '/_app' , this . _isLikeServerless ) . catch (
( ) = > { }
)
}
2022-01-20 22:25:44 +01:00
}
2022-01-14 22:01:35 +01:00
private compression =
this . nextConfig . compress && this . nextConfig . target === 'server'
? ( compression ( ) as ExpressMiddleware )
: undefined
2022-08-11 05:27:48 +02:00
protected loadEnvConfig ( {
dev ,
forceReload ,
} : {
dev : boolean
forceReload? : boolean
} ) {
loadEnvConfig ( this . dir , dev , Log , forceReload )
2022-01-20 22:25:44 +01:00
}
2022-08-12 17:25:47 +02:00
protected getResponseCache ( { dev } : { dev : boolean } ) {
const incrementalCache = new IncrementalCache ( {
fs : this.getCacheFilesystem ( ) ,
dev ,
serverDistDir : this.serverDistDir ,
maxMemoryCacheSize : this.nextConfig.experimental.isrMemoryCacheSize ,
flushToDisk :
! this . minimalMode && this . nextConfig . experimental . isrFlushToDisk ,
incrementalCacheHandlerPath :
this . nextConfig . experimental ? . incrementalCacheHandlerPath ,
getPrerenderManifest : ( ) = > {
if ( dev ) {
return {
version : - 1 as any , // letting us know this doesn't conform to spec
routes : { } ,
dynamicRoutes : { } ,
notFoundRoutes : [ ] ,
preview : null as any , // `preview` is special case read in next-dev-server
}
} else {
return this . getPrerenderManifest ( )
}
} ,
} )
return new ResponseCache ( incrementalCache , this . minimalMode )
}
2022-01-21 17:24:57 +01:00
protected getPublicDir ( ) : string {
return join ( this . dir , CLIENT_PUBLIC_FILES_PATH )
}
2021-12-07 02:14:55 +01:00
protected getHasStaticDir ( ) : boolean {
return fs . existsSync ( join ( this . dir , 'static' ) )
}
protected getPagesManifest ( ) : PagesManifest | undefined {
2022-04-27 11:50:29 +02:00
return require ( join ( this . serverDistDir , PAGES_MANIFEST ) )
2021-12-07 02:14:55 +01:00
}
2022-05-25 11:46:26 +02:00
protected getAppPathsManifest ( ) : PagesManifest | undefined {
if ( this . nextConfig . experimental . appDir ) {
const appPathsManifestPath = join ( this . serverDistDir , APP_PATHS_MANIFEST )
return require ( appPathsManifestPath )
2022-05-03 12:37:23 +02:00
}
}
2022-08-16 16:47:27 +02:00
protected async hasPage ( pathname : string ) : Promise < boolean > {
let found = false
try {
found = ! ! this . getPagePath ( pathname , this . nextConfig . i18n ? . locales )
} catch ( _ ) { }
return found
}
2021-12-07 02:14:55 +01:00
protected getBuildId ( ) : string {
const buildIdFile = join ( this . distDir , BUILD_ID_FILE )
try {
return fs . readFileSync ( buildIdFile , 'utf8' ) . trim ( )
} catch ( err ) {
if ( ! fs . existsSync ( buildIdFile ) ) {
throw new Error (
` Could not find a production build in the ' ${ this . distDir } ' directory. Try building your app with 'next build' before starting the production server. https://nextjs.org/docs/messages/production-start-no-build-id `
)
}
throw err
}
}
2022-08-16 16:47:27 +02:00
protected getCustomRoutes ( ) : CustomRoutes {
const customRoutes = this . getRoutesManifest ( )
let rewrites : CustomRoutes [ 'rewrites' ]
// rewrites can be stored as an array when an array is
// returned in next.config.js so massage them into
// the expected object format
if ( Array . isArray ( customRoutes . rewrites ) ) {
rewrites = {
beforeFiles : [ ] ,
afterFiles : customRoutes.rewrites ,
fallback : [ ] ,
}
} else {
rewrites = customRoutes . rewrites
}
return Object . assign ( customRoutes , { rewrites } )
}
2022-01-12 19:12:36 +01:00
protected generateImageRoutes ( ) : Route [ ] {
return [
{
2022-04-27 11:50:29 +02:00
match : getPathMatch ( '/_next/image' ) ,
2022-01-12 19:12:36 +01:00
type : 'route' ,
name : '_next/image catchall' ,
2022-02-09 00:46:59 +01:00
fn : async ( req , res , _params , parsedUrl ) = > {
2022-01-12 19:12:36 +01:00
if ( this . minimalMode ) {
res . statusCode = 400
2022-01-14 22:01:35 +01:00
res . body ( 'Bad Request' ) . send ( )
2022-01-12 19:12:36 +01:00
return {
finished : true ,
}
}
2022-02-09 19:37:55 +01:00
const { getHash , ImageOptimizerCache , sendResponse , ImageError } =
require ( './image-optimizer' ) as typeof import ( './image-optimizer' )
if ( ! this . imageResponseCache ) {
throw new Error (
'invariant image optimizer cache was not initialized'
)
}
2022-02-09 00:46:59 +01:00
const imagesConfig = this . nextConfig . images
2022-01-14 22:01:35 +01:00
2022-02-09 00:46:59 +01:00
if ( imagesConfig . loader !== 'default' ) {
await this . render404 ( req , res )
return { finished : true }
}
const paramsResult = ImageOptimizerCache . validateParams (
( req as NodeNextRequest ) . originalRequest ,
parsedUrl . query ,
this . nextConfig ,
! ! this . renderOpts . dev
2022-01-12 19:12:36 +01:00
)
2022-02-09 00:46:59 +01:00
if ( 'errorMessage' in paramsResult ) {
res . statusCode = 400
res . body ( paramsResult . errorMessage ) . send ( )
return { finished : true }
}
const cacheKey = ImageOptimizerCache . getCacheKey ( paramsResult )
try {
const cacheEntry = await this . imageResponseCache . get (
cacheKey ,
async ( ) = > {
const { buffer , contentType , maxAge } =
await this . imageOptimizer (
req as NodeNextRequest ,
res as NodeNextResponse ,
paramsResult
)
const etag = getHash ( [ buffer ] )
return {
value : {
kind : 'IMAGE' ,
buffer ,
etag ,
extension : getExtension ( contentType ) as string ,
} ,
revalidate : maxAge ,
}
} ,
{ }
)
if ( cacheEntry ? . value ? . kind !== 'IMAGE' ) {
throw new Error (
'invariant did not get entry from image response cache'
)
}
sendResponse (
( req as NodeNextRequest ) . originalRequest ,
( res as NodeNextResponse ) . originalResponse ,
paramsResult . href ,
cacheEntry . value . extension ,
cacheEntry . value . buffer ,
paramsResult . isStatic ,
2022-02-16 20:28:22 +01:00
cacheEntry . isMiss ? 'MISS' : cacheEntry . isStale ? 'STALE' : 'HIT' ,
2022-06-14 00:13:55 +02:00
imagesConfig . contentSecurityPolicy ,
cacheEntry . revalidate || 0 ,
Boolean ( this . renderOpts . dev )
2022-02-09 00:46:59 +01:00
)
} catch ( err ) {
if ( err instanceof ImageError ) {
res . statusCode = err . statusCode
res . body ( err . message ) . send ( )
return {
finished : true ,
}
}
throw err
}
return { finished : true }
2022-01-12 19:12:36 +01:00
} ,
} ,
]
}
2022-01-31 23:54:17 +01:00
protected generateStaticRoutes ( ) : Route [ ] {
2022-01-19 22:54:04 +01:00
return this . hasStaticDir
? [
{
// It's very important to keep this route's param optional.
// (but it should support as many params as needed, separated by '/')
// Otherwise this will lead to a pretty simple DOS attack.
// See more: https://github.com/vercel/next.js/issues/2617
2022-04-27 11:50:29 +02:00
match : getPathMatch ( '/static/:path*' ) ,
2022-01-19 22:54:04 +01:00
name : 'static catchall' ,
fn : async ( req , res , params , parsedUrl ) = > {
const p = join ( this . dir , 'static' , . . . params . path )
await this . serveStatic ( req , res , p , parsedUrl )
return {
finished : true ,
}
} ,
} as Route ,
]
: [ ]
}
2022-01-26 07:22:11 +01:00
protected setImmutableAssetCacheControl ( res : BaseNextResponse ) : void {
res . setHeader ( 'Cache-Control' , 'public, max-age=31536000, immutable' )
}
2022-01-19 22:54:04 +01:00
protected generateFsStaticRoutes ( ) : Route [ ] {
return [
{
2022-04-27 11:50:29 +02:00
match : getPathMatch ( '/_next/static/:path*' ) ,
2022-01-19 22:54:04 +01:00
type : 'route' ,
name : '_next/static catchall' ,
fn : async ( req , res , params , parsedUrl ) = > {
// make sure to 404 for /_next/static itself
if ( ! params . path ) {
await this . render404 ( req , res , parsedUrl )
return {
finished : true ,
}
}
if (
params . path [ 0 ] === CLIENT_STATIC_FILES_RUNTIME ||
params . path [ 0 ] === 'chunks' ||
params . path [ 0 ] === 'css' ||
params . path [ 0 ] === 'image' ||
params . path [ 0 ] === 'media' ||
params . path [ 0 ] === this . buildId ||
params . path [ 0 ] === 'pages' ||
params . path [ 1 ] === 'pages'
) {
this . setImmutableAssetCacheControl ( res )
}
const p = join (
this . distDir ,
CLIENT_STATIC_FILES_PATH ,
. . . ( params . path || [ ] )
)
await this . serveStatic ( req , res , p , parsedUrl )
return {
finished : true ,
}
} ,
} ,
]
}
2021-12-07 02:14:55 +01:00
protected generatePublicRoutes ( ) : Route [ ] {
if ( ! fs . existsSync ( this . publicDir ) ) return [ ]
const publicFiles = new Set (
recursiveReadDirSync ( this . publicDir ) . map ( ( p ) = >
encodeURI ( p . replace ( /\\/g , '/' ) )
)
)
return [
{
2022-04-27 11:50:29 +02:00
match : getPathMatch ( '/:path*' ) ,
2022-06-16 23:43:01 +02:00
matchesBasePath : true ,
2021-12-07 02:14:55 +01:00
name : 'public folder catchall' ,
fn : async ( req , res , params , parsedUrl ) = > {
const pathParts : string [ ] = params . path || [ ]
const { basePath } = this . nextConfig
// if basePath is defined require it be present
if ( basePath ) {
const basePathParts = basePath . split ( '/' )
// remove first empty value
basePathParts . shift ( )
if (
! basePathParts . every ( ( part : string , idx : number ) = > {
return part === pathParts [ idx ]
} )
) {
return { finished : false }
}
pathParts . splice ( 0 , basePathParts . length )
}
let path = ` / ${ pathParts . join ( '/' ) } `
if ( ! publicFiles . has ( path ) ) {
// In `next-dev-server.ts`, we ensure encoded paths match
// decoded paths on the filesystem. So we need do the
// opposite here: make sure decoded paths match encoded.
path = encodeURI ( path )
}
if ( publicFiles . has ( path ) ) {
await this . serveStatic (
req ,
res ,
join ( this . publicDir , . . . pathParts ) ,
parsedUrl
)
return {
finished : true ,
}
}
return {
finished : false ,
}
} ,
} as Route ,
]
}
private _validFilesystemPathSet : Set < string > | null = null
protected getFilesystemPaths ( ) : Set < string > {
if ( this . _validFilesystemPathSet ) {
return this . _validFilesystemPathSet
}
const pathUserFilesStatic = join ( this . dir , 'static' )
let userFilesStatic : string [ ] = [ ]
if ( this . hasStaticDir && fs . existsSync ( pathUserFilesStatic ) ) {
userFilesStatic = recursiveReadDirSync ( pathUserFilesStatic ) . map ( ( f ) = >
join ( '.' , 'static' , f )
)
}
let userFilesPublic : string [ ] = [ ]
if ( this . publicDir && fs . existsSync ( this . publicDir ) ) {
userFilesPublic = recursiveReadDirSync ( this . publicDir ) . map ( ( f ) = >
join ( '.' , 'public' , f )
)
}
let nextFilesStatic : string [ ] = [ ]
nextFilesStatic =
! this . minimalMode && fs . existsSync ( join ( this . distDir , 'static' ) )
? recursiveReadDirSync ( join ( this . distDir , 'static' ) ) . map ( ( f ) = >
join ( '.' , relative ( this . dir , this . distDir ) , 'static' , f )
)
: [ ]
return ( this . _validFilesystemPathSet = new Set < string > ( [
. . . nextFilesStatic ,
. . . userFilesPublic ,
. . . userFilesStatic ,
] ) )
}
2021-12-17 23:56:26 +01:00
2022-01-14 22:01:35 +01:00
protected sendRenderResult (
req : NodeNextRequest ,
res : NodeNextResponse ,
options : {
result : RenderResult
type : 'html' | 'json'
generateEtags : boolean
poweredByHeader : boolean
options? : PayloadOptions | undefined
}
) : Promise < void > {
return sendRenderResult ( {
req : req.originalRequest ,
res : res.originalResponse ,
. . . options ,
} )
}
protected sendStatic (
req : NodeNextRequest ,
res : NodeNextResponse ,
path : string
) : Promise < void > {
return serveStatic ( req . originalRequest , res . originalResponse , path )
}
protected handleCompression (
req : NodeNextRequest ,
res : NodeNextResponse
) : void {
if ( this . compression ) {
this . compression ( req . originalRequest , res . originalResponse , ( ) = > { } )
}
}
2022-08-10 19:00:30 +02:00
protected async handleUpgrade ( req : NodeNextRequest , socket : any , head : any ) {
await this . router . execute ( req , socket , nodeParseUrl ( req . url , true ) , head )
}
2022-01-14 22:01:35 +01:00
protected async proxyRequest (
req : NodeNextRequest ,
res : NodeNextResponse ,
2022-08-10 19:00:30 +02:00
parsedUrl : ParsedUrl ,
upgradeHead? : any
2022-01-14 22:01:35 +01:00
) {
const { query } = parsedUrl
delete ( parsedUrl as any ) . query
parsedUrl . search = stringifyQuery ( req , query )
const target = formatUrl ( parsedUrl )
2022-02-18 20:43:43 +01:00
const proxy = new HttpProxy ( {
2022-01-14 22:01:35 +01:00
target ,
changeOrigin : true ,
ignorePath : true ,
xfwd : true ,
2022-08-10 19:00:30 +02:00
ws : true ,
// we limit proxy requests to 30s by default, in development
// we don't time out WebSocket requests to allow proxying
proxyTimeout : upgradeHead && this . renderOpts . dev ? undefined : 30 _000 ,
2022-01-14 22:01:35 +01:00
} )
await new Promise ( ( proxyResolve , proxyReject ) = > {
let finished = false
proxy . on ( 'error' , ( err ) = > {
2022-08-10 19:00:30 +02:00
console . error ( ` Failed to proxy ${ target } ` , err )
2022-01-14 22:01:35 +01:00
if ( ! finished ) {
finished = true
proxyReject ( err )
}
} )
2022-08-10 19:00:30 +02:00
// if upgrade head is present treat as WebSocket request
if ( upgradeHead ) {
proxy . on ( 'proxyReqWs' , ( proxyReq ) = > {
proxyReq . on ( 'close' , ( ) = > {
if ( ! finished ) {
finished = true
proxyResolve ( true )
}
} )
} )
proxy . ws ( req as any as IncomingMessage , res , upgradeHead )
proxyResolve ( true )
} else {
proxy . on ( 'proxyReq' , ( proxyReq ) = > {
proxyReq . on ( 'close' , ( ) = > {
if ( ! finished ) {
finished = true
proxyResolve ( true )
}
} )
} )
proxy . web ( req . originalRequest , res . originalResponse )
}
2022-01-14 22:01:35 +01:00
} )
return {
finished : true ,
}
}
protected async runApi (
2022-06-21 21:04:48 +02:00
req : BaseNextRequest | NodeNextRequest ,
res : BaseNextResponse | NodeNextResponse ,
2022-01-14 22:01:35 +01:00
query : ParsedUrlQuery ,
2022-06-21 21:04:48 +02:00
params : Params | undefined ,
2022-01-14 22:01:35 +01:00
page : string ,
builtPagePath : string
) : Promise < boolean > {
2022-06-21 21:04:48 +02:00
const handledAsEdgeFunction = await this . runEdgeFunction ( {
2022-06-13 20:17:44 +02:00
req ,
res ,
query ,
params ,
page ,
} )
if ( handledAsEdgeFunction ) {
return true
}
2022-01-14 22:01:35 +01:00
const pageModule = await require ( builtPagePath )
query = { . . . query , . . . params }
delete query . __nextLocale
delete query . __nextDefaultLocale
if ( ! this . renderOpts . dev && this . _isLikeServerless ) {
if ( typeof pageModule . default === 'function' ) {
prepareServerlessUrl ( req , query )
await pageModule . default ( req , res )
return true
}
}
await apiResolver (
2022-06-21 21:04:48 +02:00
( req as NodeNextRequest ) . originalRequest ,
( res as NodeNextResponse ) . originalResponse ,
2022-01-14 22:01:35 +01:00
query ,
pageModule ,
2022-02-08 04:50:23 +01:00
{
. . . this . renderOpts . previewProps ,
2022-03-17 18:06:44 +01:00
revalidate : ( newReq : IncomingMessage , newRes : ServerResponse ) = >
this . getRequestHandler ( ) (
new NodeNextRequest ( newReq ) ,
new NodeNextResponse ( newRes )
) ,
2022-02-08 04:50:23 +01:00
// internal config so is not typed
trustHostHeader : ( this . nextConfig . experimental as any ) . trustHostHeader ,
} ,
2022-01-14 22:01:35 +01:00
this . minimalMode ,
this . renderOpts . dev ,
page
)
return true
}
protected async renderHTML (
req : NodeNextRequest ,
res : NodeNextResponse ,
pathname : string ,
query : NextParsedUrlQuery ,
renderOpts : RenderOpts
) : Promise < RenderResult | null > {
2022-02-08 14:16:46 +01:00
// Due to the way we pass data by mutating `renderOpts`, we can't extend the
// object here but only updating its `serverComponentManifest` field.
// https://github.com/vercel/next.js/blob/df7cbd904c3bd85f399d1ce90680c0ecf92d2752/packages/next/server/render.tsx#L947-L952
renderOpts . serverComponentManifest = this . serverComponentManifest
2022-08-12 15:01:19 +02:00
renderOpts . serverCSSManifest = this . serverCSSManifest
2022-02-08 14:16:46 +01:00
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
if (
this . nextConfig . experimental . appDir &&
( renderOpts . isAppPath || query . __flight__ )
) {
const isPagesDir = ! renderOpts . isAppPath
2022-07-26 12:41:51 +02:00
return appRenderToHTMLOrFlight (
2022-05-03 12:37:23 +02:00
req . originalRequest ,
res . originalResponse ,
pathname ,
query ,
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
renderOpts ,
isPagesDir
2022-05-03 12:37:23 +02:00
)
}
2022-01-14 22:01:35 +01:00
return renderToHTML (
req . originalRequest ,
res . originalResponse ,
pathname ,
query ,
renderOpts
)
}
protected streamResponseChunk ( res : NodeNextResponse , chunk : any ) {
res . originalResponse . write ( chunk )
2022-02-24 13:43:22 +01:00
// When both compression and streaming are enabled, we need to explicitly
// flush the response to avoid it being buffered by gzip.
if ( this . compression && 'flush' in res . originalResponse ) {
; ( res . originalResponse as any ) . flush ( )
}
2022-01-14 22:01:35 +01:00
}
protected async imageOptimizer (
req : NodeNextRequest ,
res : NodeNextResponse ,
2022-02-09 00:46:59 +01:00
paramsResult : import ( './image-optimizer' ) . ImageParamsResult
) : Promise < { buffer : Buffer ; contentType : string ; maxAge : number } > {
2022-01-14 22:01:35 +01:00
const { imageOptimizer } =
require ( './image-optimizer' ) as typeof import ( './image-optimizer' )
return imageOptimizer (
req . originalRequest ,
res . originalResponse ,
2022-02-09 00:46:59 +01:00
paramsResult ,
2022-01-14 22:01:35 +01:00
this . nextConfig ,
( newReq , newRes , newParsedUrl ) = >
this . getRequestHandler ( ) (
new NodeNextRequest ( newReq ) ,
new NodeNextResponse ( newRes ) ,
newParsedUrl
2022-02-09 00:46:59 +01:00
)
2022-01-14 22:01:35 +01:00
)
}
2022-01-12 19:12:36 +01:00
protected getPagePath ( pathname : string , locales? : string [ ] ) : string {
return getPagePath (
pathname ,
this . distDir ,
this . _isLikeServerless ,
this . renderOpts . dev ,
2022-05-03 12:37:23 +02:00
locales ,
2022-05-25 11:46:26 +02:00
this . nextConfig . experimental . appDir
2022-01-12 19:12:36 +01:00
)
}
protected async findPageComponents (
pathname : string ,
query : NextParsedUrlQuery = { } ,
2022-07-09 14:33:51 +02:00
params : Params | null = null ,
isAppDir : boolean = false
2022-01-12 19:12:36 +01:00
) : Promise < FindComponentsResult | null > {
let paths = [
// try serving a static AMP version first
query . amp ? normalizePagePath ( pathname ) + '.amp' : null ,
pathname ,
] . filter ( Boolean )
if ( query . __nextLocale ) {
paths = [
. . . paths . map (
( path ) = > ` / ${ query . __nextLocale } ${ path === '/' ? '' : path } `
) ,
. . . paths ,
]
}
for ( const pagePath of paths ) {
try {
const components = await loadComponents (
this . distDir ,
pagePath ! ,
2022-04-05 21:46:17 +02:00
! this . renderOpts . dev && this . _isLikeServerless ,
2022-05-03 12:37:23 +02:00
this . renderOpts . serverComponents ,
2022-05-25 11:46:26 +02:00
this . nextConfig . experimental . appDir
2022-01-12 19:12:36 +01:00
)
if (
query . __nextLocale &&
typeof components . Component === 'string' &&
! pagePath ? . startsWith ( ` / ${ query . __nextLocale } ` )
) {
// if loading an static HTML file the locale is required
// to be present since all HTML files are output under their locale
continue
}
return {
components ,
query : {
. . . ( components . getStaticProps
? ( {
amp : query.amp ,
2022-06-10 19:35:12 +02:00
__nextDataReq : query.__nextDataReq ,
2022-01-12 19:12:36 +01:00
__nextLocale : query.__nextLocale ,
__nextDefaultLocale : query.__nextDefaultLocale ,
2022-04-01 18:13:38 +02:00
__flight__ : query.__flight__ ,
2022-01-12 19:12:36 +01:00
} as NextParsedUrlQuery )
: query ) ,
2022-07-09 14:33:51 +02:00
// For appDir params is excluded.
. . . ( ( isAppDir ? { } : params ) || { } ) ,
2022-01-12 19:12:36 +01:00
} ,
}
} catch ( err ) {
2022-06-06 20:35:26 +02:00
// we should only not throw if we failed to find the page
// in the pages-manifest
if ( ! ( err instanceof PageNotFoundError ) ) {
throw err
}
2022-01-12 19:12:36 +01:00
}
}
return null
}
protected getFontManifest ( ) : FontManifest {
return requireFontManifest ( this . distDir , this . _isLikeServerless )
}
2022-02-08 14:16:46 +01:00
protected getServerComponentManifest() {
2022-03-26 00:05:35 +01:00
if ( ! this . nextConfig . experimental . serverComponents ) return undefined
2022-06-26 23:01:26 +02:00
return require ( join ( this . distDir , 'server' , FLIGHT_MANIFEST + '.json' ) )
2022-02-08 14:16:46 +01:00
}
2022-08-12 15:01:19 +02:00
protected getServerCSSManifest() {
if ( ! this . nextConfig . experimental . serverComponents ) return undefined
return require ( join (
this . distDir ,
'server' ,
FLIGHT_SERVER_CSS_MANIFEST + '.json'
) )
}
2022-08-12 17:25:47 +02:00
protected getFallback ( page : string ) : Promise < string > {
page = normalizePagePath ( page )
const cacheFs = this . getCacheFilesystem ( )
return cacheFs . readFile ( join ( this . serverDistDir , 'pages' , ` ${ page } .html ` ) )
}
2022-08-16 16:47:27 +02:00
protected generateRoutes ( ) : {
headers : Route [ ]
rewrites : {
beforeFiles : Route [ ]
afterFiles : Route [ ]
fallback : Route [ ]
}
fsRoutes : Route [ ]
redirects : Route [ ]
catchAllRoute : Route
catchAllMiddleware : Route [ ]
pageChecker : PageChecker
useFileSystemPublicRoutes : boolean
dynamicRoutes : DynamicRoutes | undefined
nextConfig : NextConfig
} {
const publicRoutes = this . generatePublicRoutes ( )
const imageRoutes = this . generateImageRoutes ( )
const staticFilesRoutes = this . generateStaticRoutes ( )
const fsRoutes : Route [ ] = [
. . . this . generateFsStaticRoutes ( ) ,
{
match : getPathMatch ( '/_next/data/:path*' ) ,
type : 'route' ,
name : '_next/data catchall' ,
check : true ,
fn : async ( req , res , params , _parsedUrl ) = > {
// Make sure to 404 for /_next/data/ itself and
// we also want to 404 if the buildId isn't correct
if ( ! params . path || params . path [ 0 ] !== this . buildId ) {
await this . render404 ( req , res , _parsedUrl )
return {
finished : true ,
}
}
// remove buildId from URL
params . path . shift ( )
const lastParam = params . path [ params . path . length - 1 ]
// show 404 if it doesn't end with .json
if ( typeof lastParam !== 'string' || ! lastParam . endsWith ( '.json' ) ) {
await this . render404 ( req , res , _parsedUrl )
return {
finished : true ,
}
}
// re-create page's pathname
let pathname = ` / ${ params . path . join ( '/' ) } `
pathname = getRouteFromAssetPath ( pathname , '.json' )
// ensure trailing slash is normalized per config
if ( this . router . catchAllMiddleware [ 0 ] ) {
if ( this . nextConfig . trailingSlash && ! pathname . endsWith ( '/' ) ) {
pathname += '/'
}
if (
! this . nextConfig . trailingSlash &&
pathname . length > 1 &&
pathname . endsWith ( '/' )
) {
pathname = pathname . substring ( 0 , pathname . length - 1 )
}
}
if ( this . nextConfig . i18n ) {
const { host } = req ? . headers || { }
// remove port from host and remove port if present
const hostname = host ? . split ( ':' ) [ 0 ] . toLowerCase ( )
const localePathResult = normalizeLocalePath (
pathname ,
this . nextConfig . i18n . locales
)
const { defaultLocale } =
detectDomainLocale ( this . nextConfig . i18n . domains , hostname ) || { }
let detectedLocale = ''
if ( localePathResult . detectedLocale ) {
pathname = localePathResult . pathname
detectedLocale = localePathResult . detectedLocale
}
_parsedUrl . query . __nextLocale = detectedLocale
_parsedUrl . query . __nextDefaultLocale =
defaultLocale || this . nextConfig . i18n . defaultLocale
if ( ! detectedLocale && ! this . router . catchAllMiddleware [ 0 ] ) {
_parsedUrl . query . __nextLocale =
_parsedUrl . query . __nextDefaultLocale
await this . render404 ( req , res , _parsedUrl )
return { finished : true }
}
}
return {
pathname ,
query : { . . . _parsedUrl . query , __nextDataReq : '1' } ,
finished : false ,
}
} ,
} ,
. . . imageRoutes ,
{
match : getPathMatch ( '/_next/:path*' ) ,
type : 'route' ,
name : '_next catchall' ,
// This path is needed because `render()` does a check for `/_next` and the calls the routing again
fn : async ( req , res , _params , parsedUrl ) = > {
await this . render404 ( req , res , parsedUrl )
return {
finished : true ,
}
} ,
} ,
. . . publicRoutes ,
. . . staticFilesRoutes ,
]
const restrictedRedirectPaths = this . nextConfig . basePath
? [ ` ${ this . nextConfig . basePath } /_next ` ]
: [ '/_next' ]
// Headers come very first
const headers = this . minimalMode
? [ ]
: this . customRoutes . headers . map ( ( rule ) = >
createHeaderRoute ( { rule , restrictedRedirectPaths } )
)
const redirects = this . minimalMode
? [ ]
: this . customRoutes . redirects . map ( ( rule ) = >
createRedirectRoute ( { rule , restrictedRedirectPaths } )
)
const rewrites = this . generateRewrites ( { restrictedRedirectPaths } )
const catchAllMiddleware = this . generateCatchAllMiddlewareRoute ( )
const catchAllRoute : Route = {
match : getPathMatch ( '/:path*' ) ,
type : 'route' ,
matchesLocale : true ,
name : 'Catchall render' ,
fn : async ( req , res , _params , parsedUrl ) = > {
let { pathname , query } = parsedUrl
if ( ! pathname ) {
throw new Error ( 'pathname is undefined' )
}
// next.js core assumes page path without trailing slash
pathname = removeTrailingSlash ( pathname )
if ( this . nextConfig . i18n ) {
const localePathResult = normalizeLocalePath (
pathname ,
this . nextConfig . i18n ? . locales
)
if ( localePathResult . detectedLocale ) {
pathname = localePathResult . pathname
parsedUrl . query . __nextLocale = localePathResult . detectedLocale
}
}
const bubbleNoFallback = ! ! query . _nextBubbleNoFallback
if ( pathname === '/api' || pathname . startsWith ( '/api/' ) ) {
delete query . _nextBubbleNoFallback
const handled = await this . handleApiRequest ( req , res , pathname , query )
if ( handled ) {
return { finished : true }
}
}
try {
await this . render ( req , res , pathname , query , parsedUrl , true )
return {
finished : true ,
}
} catch ( err ) {
if ( err instanceof NoFallbackError && bubbleNoFallback ) {
return {
finished : false ,
}
}
throw err
}
} ,
}
const { useFileSystemPublicRoutes } = this . nextConfig
if ( useFileSystemPublicRoutes ) {
this . appPathRoutes = this . getAppPathRoutes ( )
this . dynamicRoutes = this . getDynamicRoutes ( )
}
return {
headers ,
fsRoutes ,
rewrites ,
redirects ,
catchAllRoute ,
catchAllMiddleware ,
useFileSystemPublicRoutes ,
dynamicRoutes : this.dynamicRoutes ,
pageChecker : this.hasPage.bind ( this ) ,
nextConfig : this.nextConfig ,
}
}
// Used to build API page in development
protected async ensureApiPage ( _pathname : string ) : Promise < void > { }
/ * *
* Resolves ` API ` request , in development builds on demand
* @param req http request
* @param res http response
* @param pathname path of request
* /
protected async handleApiRequest (
req : BaseNextRequest ,
res : BaseNextResponse ,
pathname : string ,
query : ParsedUrlQuery
) : Promise < boolean > {
let page = pathname
let params : Params | undefined = undefined
let pageFound = ! isDynamicRoute ( page ) && ( await this . hasPage ( page ) )
if ( ! pageFound && this . dynamicRoutes ) {
for ( const dynamicRoute of this . dynamicRoutes ) {
params = dynamicRoute . match ( pathname ) || undefined
if ( dynamicRoute . page . startsWith ( '/api' ) && params ) {
page = dynamicRoute . page
pageFound = true
break
}
}
}
if ( ! pageFound ) {
return false
}
// Make sure the page is built before getting the path
// or else it won't be in the manifest yet
await this . ensureApiPage ( page )
let builtPagePath
try {
builtPagePath = this . getPagePath ( page )
} catch ( err ) {
if ( isError ( err ) && err . code === 'ENOENT' ) {
return false
}
throw err
}
return this . runApi ( req , res , query , params , page , builtPagePath )
}
2021-12-17 23:56:26 +01:00
protected getCacheFilesystem ( ) : CacheFs {
return {
readFile : ( f ) = > fs . promises . readFile ( f , 'utf8' ) ,
readFileSync : ( f ) = > fs . readFileSync ( f , 'utf8' ) ,
writeFile : ( f , d ) = > fs . promises . writeFile ( f , d , 'utf8' ) ,
mkdir : ( dir ) = > fs . promises . mkdir ( dir , { recursive : true } ) ,
stat : ( f ) = > fs . promises . stat ( f ) ,
}
}
2022-01-12 19:12:36 +01:00
2022-01-14 22:01:35 +01:00
private normalizeReq (
req : BaseNextRequest | IncomingMessage
) : BaseNextRequest {
return req instanceof IncomingMessage ? new NodeNextRequest ( req ) : req
}
private normalizeRes (
res : BaseNextResponse | ServerResponse
) : BaseNextResponse {
return res instanceof ServerResponse ? new NodeNextResponse ( res ) : res
}
public getRequestHandler ( ) : NodeRequestHandler {
const handler = super . getRequestHandler ( )
return async ( req , res , parsedUrl ) = > {
return handler ( this . normalizeReq ( req ) , this . normalizeRes ( res ) , parsedUrl )
}
}
public async render (
req : BaseNextRequest | IncomingMessage ,
res : BaseNextResponse | ServerResponse ,
pathname : string ,
query? : NextParsedUrlQuery ,
2022-01-21 21:38:59 +01:00
parsedUrl? : NextUrlWithParsedQuery ,
internal = false
2022-01-14 22:01:35 +01:00
) : Promise < void > {
return super . render (
this . normalizeReq ( req ) ,
this . normalizeRes ( res ) ,
pathname ,
query ,
2022-01-21 21:38:59 +01:00
parsedUrl ,
internal
2022-01-14 22:01:35 +01:00
)
}
public async renderToHTML (
req : BaseNextRequest | IncomingMessage ,
res : BaseNextResponse | ServerResponse ,
pathname : string ,
query? : ParsedUrlQuery
) : Promise < string | null > {
return super . renderToHTML (
this . normalizeReq ( req ) ,
this . normalizeRes ( res ) ,
pathname ,
query
)
}
public async renderError (
err : Error | null ,
req : BaseNextRequest | IncomingMessage ,
res : BaseNextResponse | ServerResponse ,
pathname : string ,
query? : NextParsedUrlQuery ,
setHeaders? : boolean
) : Promise < void > {
return super . renderError (
err ,
this . normalizeReq ( req ) ,
this . normalizeRes ( res ) ,
pathname ,
query ,
setHeaders
)
}
public async renderErrorToHTML (
err : Error | null ,
req : BaseNextRequest | IncomingMessage ,
res : BaseNextResponse | ServerResponse ,
pathname : string ,
query? : ParsedUrlQuery
) : Promise < string | null > {
return super . renderErrorToHTML (
err ,
this . normalizeReq ( req ) ,
this . normalizeRes ( res ) ,
pathname ,
query
)
}
public async render404 (
req : BaseNextRequest | IncomingMessage ,
res : BaseNextResponse | ServerResponse ,
parsedUrl? : NextUrlWithParsedQuery ,
setHeaders? : boolean
) : Promise < void > {
return super . render404 (
this . normalizeReq ( req ) ,
this . normalizeRes ( res ) ,
parsedUrl ,
setHeaders
)
}
public async serveStatic (
req : BaseNextRequest | IncomingMessage ,
res : BaseNextResponse | ServerResponse ,
path : string ,
parsedUrl? : UrlWithParsedQuery
) : Promise < void > {
2022-01-19 22:54:04 +01:00
if ( ! this . isServeableUrl ( path ) ) {
return this . render404 ( req , res , parsedUrl )
}
if ( ! ( req . method === 'GET' || req . method === 'HEAD' ) ) {
res . statusCode = 405
res . setHeader ( 'Allow' , [ 'GET' , 'HEAD' ] )
return this . renderError ( null , req , res , path )
}
try {
await this . sendStatic (
req as NodeNextRequest ,
res as NodeNextResponse ,
path
)
} catch ( error ) {
if ( ! isError ( error ) ) throw error
const err = error as Error & { code? : string ; statusCode? : number }
if ( err . code === 'ENOENT' || err . statusCode === 404 ) {
this . render404 ( req , res , parsedUrl )
} else if ( err . statusCode === 412 ) {
res . statusCode = 412
return this . renderError ( err , req , res , path )
} else {
throw err
}
}
}
protected getStaticRoutes ( ) : Route [ ] {
return this . hasStaticDir
? [
{
// It's very important to keep this route's param optional.
// (but it should support as many params as needed, separated by '/')
// Otherwise this will lead to a pretty simple DOS attack.
// See more: https://github.com/vercel/next.js/issues/2617
2022-04-27 11:50:29 +02:00
match : getPathMatch ( '/static/:path*' ) ,
2022-01-19 22:54:04 +01:00
name : 'static catchall' ,
fn : async ( req , res , params , parsedUrl ) = > {
const p = join ( this . dir , 'static' , . . . params . path )
await this . serveStatic ( req , res , p , parsedUrl )
return {
finished : true ,
}
} ,
} as Route ,
]
: [ ]
}
protected isServeableUrl ( untrustedFileUrl : string ) : boolean {
// This method mimics what the version of `send` we use does:
// 1. decodeURIComponent:
// https://github.com/pillarjs/send/blob/0.17.1/index.js#L989
// https://github.com/pillarjs/send/blob/0.17.1/index.js#L518-L522
// 2. resolve:
// https://github.com/pillarjs/send/blob/de073ed3237ade9ff71c61673a34474b30e5d45b/index.js#L561
let decodedUntrustedFilePath : string
try {
// (1) Decode the URL so we have the proper file name
decodedUntrustedFilePath = decodeURIComponent ( untrustedFileUrl )
} catch {
return false
}
// (2) Resolve "up paths" to determine real request
const untrustedFilePath = resolve ( decodedUntrustedFilePath )
// don't allow null bytes anywhere in the file path
if ( untrustedFilePath . indexOf ( '\0' ) !== - 1 ) {
return false
}
// Check if .next/static, static and public are in the path.
// If not the path is not available.
if (
( untrustedFilePath . startsWith ( join ( this . distDir , 'static' ) + sep ) ||
untrustedFilePath . startsWith ( join ( this . dir , 'static' ) + sep ) ||
untrustedFilePath . startsWith ( join ( this . dir , 'public' ) + sep ) ) === false
) {
return false
}
// Check against the real filesystem paths
const filesystemUrls = this . getFilesystemPaths ( )
const resolved = relative ( this . dir , untrustedFilePath )
return filesystemUrls . has ( resolved )
2022-01-14 22:01:35 +01:00
}
2022-01-26 07:22:11 +01:00
protected generateRewrites ( {
restrictedRedirectPaths ,
} : {
restrictedRedirectPaths : string [ ]
} ) {
let beforeFiles : Route [ ] = [ ]
let afterFiles : Route [ ] = [ ]
let fallback : Route [ ] = [ ]
if ( ! this . minimalMode ) {
2022-06-16 23:43:01 +02:00
const buildRewrite = ( rewrite : Rewrite , check = true ) : Route = > {
2022-01-26 07:22:11 +01:00
const rewriteRoute = getCustomRoute ( {
type : 'rewrite' ,
rule : rewrite ,
restrictedRedirectPaths ,
} )
return {
. . . rewriteRoute ,
check ,
type : rewriteRoute . type ,
name : ` Rewrite route ${ rewriteRoute . source } ` ,
match : rewriteRoute.match ,
2022-06-16 23:43:01 +02:00
matchesBasePath : true ,
matchesLocale : true ,
matchesLocaleAPIRoutes : true ,
matchesTrailingSlash : true ,
2022-08-10 19:00:30 +02:00
fn : async ( req , res , params , parsedUrl , upgradeHead ) = > {
2022-01-26 07:22:11 +01:00
const { newUrl , parsedDestination } = prepareDestination ( {
appendParamsToQuery : true ,
destination : rewriteRoute.destination ,
params : params ,
query : parsedUrl.query ,
} )
// external rewrite, proxy it
if ( parsedDestination . protocol ) {
return this . proxyRequest (
req as NodeNextRequest ,
res as NodeNextResponse ,
2022-08-10 19:00:30 +02:00
parsedDestination ,
upgradeHead
2022-01-26 07:22:11 +01:00
)
}
addRequestMeta ( req , '_nextRewroteUrl' , newUrl )
addRequestMeta ( req , '_nextDidRewrite' , newUrl !== req . url )
return {
finished : false ,
pathname : newUrl ,
query : parsedDestination.query ,
}
} ,
2022-06-16 23:43:01 +02:00
}
2022-01-26 07:22:11 +01:00
}
if ( Array . isArray ( this . customRoutes . rewrites ) ) {
afterFiles = this . customRoutes . rewrites . map ( ( r ) = > buildRewrite ( r ) )
} else {
beforeFiles = this . customRoutes . rewrites . beforeFiles . map ( ( r ) = >
buildRewrite ( r , false )
)
afterFiles = this . customRoutes . rewrites . afterFiles . map ( ( r ) = >
buildRewrite ( r )
)
fallback = this . customRoutes . rewrites . fallback . map ( ( r ) = >
buildRewrite ( r )
)
}
}
return {
beforeFiles ,
afterFiles ,
fallback ,
}
}
2022-06-21 21:04:48 +02:00
protected getMiddlewareManifest ( ) : MiddlewareManifest | null {
if ( this . minimalMode ) return null
const manifest : MiddlewareManifest = require ( join (
this . serverDistDir ,
MIDDLEWARE_MANIFEST
) )
return manifest
}
2022-05-19 17:46:21 +02:00
/ * *
* Return a list of middleware routing items . This method exists to be later
* overridden by the development server in order to use a different source
* to get the list .
* /
2022-07-30 01:37:59 +02:00
protected getMiddleware ( ) : RoutingItem | undefined {
2022-06-21 21:04:48 +02:00
const manifest = this . getMiddlewareManifest ( )
2022-07-30 23:45:58 +02:00
const rootMiddleware = manifest ? . middleware ? . [ '/' ]
if ( ! rootMiddleware ) {
2022-07-30 01:37:59 +02:00
return
2022-05-19 17:46:21 +02:00
}
2022-07-30 23:45:58 +02:00
return {
match : getMiddlewareMatcher ( rootMiddleware ) ,
page : '/' ,
}
2022-05-19 17:46:21 +02:00
}
2022-06-21 21:04:48 +02:00
protected getEdgeFunctions ( ) : RoutingItem [ ] {
const manifest = this . getMiddlewareManifest ( )
if ( ! manifest ) {
return [ ]
}
2022-08-14 23:51:11 +02:00
// Make sure to sort function routes too.
return getSortedRoutes ( Object . keys ( manifest . functions ) ) . map ( ( page ) = > {
return {
match : getMiddlewareMatcher ( manifest . functions [ page ] ) ,
page ,
}
} )
2022-06-21 21:04:48 +02:00
}
2022-05-19 17:46:21 +02:00
/ * *
2022-06-13 20:17:44 +02:00
* Get information for the edge function located in the provided page
* folder . If the edge function info can ' t be found it will throw
2022-05-19 17:46:21 +02:00
* an error .
* /
2022-06-13 20:17:44 +02:00
protected getEdgeFunctionInfo ( params : {
page : string
/** Whether we should look for a middleware or not */
middleware : boolean
} ) {
2022-05-19 17:46:21 +02:00
const manifest : MiddlewareManifest = require ( join (
this . serverDistDir ,
MIDDLEWARE_MANIFEST
) )
let foundPage : string
try {
2022-06-13 20:17:44 +02:00
foundPage = denormalizePagePath ( normalizePagePath ( params . page ) )
2022-05-19 17:46:21 +02:00
} catch ( err ) {
2022-06-24 20:50:49 +02:00
return null
2022-05-19 17:46:21 +02:00
}
2022-06-13 20:17:44 +02:00
let pageInfo = params . middleware
? manifest . middleware [ foundPage ]
: manifest . functions [ foundPage ]
2022-06-24 20:50:49 +02:00
2022-05-19 17:46:21 +02:00
if ( ! pageInfo ) {
2022-06-24 20:50:49 +02:00
if ( ! params . middleware ) {
throw new PageNotFoundError ( foundPage )
}
return null
2022-05-19 17:46:21 +02:00
}
return {
name : pageInfo.name ,
paths : pageInfo.files.map ( ( file ) = > join ( this . distDir , file ) ) ,
env : pageInfo.env ? ? [ ] ,
wasm : ( pageInfo . wasm ? ? [ ] ) . map ( ( binding ) = > ( {
. . . binding ,
filePath : join ( this . distDir , binding . filePath ) ,
} ) ) ,
2022-07-19 19:27:15 +02:00
assets : ( pageInfo . assets ? ? [ ] ) . map ( ( binding ) = > {
return {
. . . binding ,
filePath : join ( this . distDir , binding . filePath ) ,
}
} ) ,
2022-05-19 17:46:21 +02:00
}
}
/ * *
* Checks if a middleware exists . This method is useful for the development
* server where we need to check the filesystem . Here we just check the
* middleware manifest .
* /
2022-07-30 23:45:58 +02:00
protected async hasMiddleware ( pathname : string ) : Promise < boolean > {
2022-06-24 20:50:49 +02:00
const info = this . getEdgeFunctionInfo ( { page : pathname , middleware : true } )
return Boolean ( info && info . paths . length > 0 )
2022-05-19 17:46:21 +02:00
}
/ * *
* A placeholder for a function to be defined in the development server .
2022-07-30 23:45:58 +02:00
* It will make sure that the root middleware or an edge function has been compiled
* so that we can run it .
2022-05-19 17:46:21 +02:00
* /
2022-07-30 23:45:58 +02:00
protected async ensureMiddleware() { }
protected async ensureEdgeFunction ( _pathname : string ) { }
2022-05-19 17:46:21 +02:00
/ * *
* This method gets all middleware matchers and execute them when the request
* matches . It will make sure that each middleware exists and is compiled and
* ready to be invoked . The development server will decorate it to add warns
* and errors with rich traces .
* /
protected async runMiddleware ( params : {
request : BaseNextRequest
response : BaseNextResponse
2022-05-27 20:29:04 +02:00
parsedUrl : ParsedUrl
2022-05-19 17:46:21 +02:00
parsed : UrlWithParsedQuery
onWarning ? : ( warning : Error ) = > void
2022-06-15 02:58:13 +02:00
} ) {
2022-06-24 17:43:36 +02:00
// Middleware is skipped for on-demand revalidate requests
2022-06-16 18:22:35 +02:00
if (
checkIsManualRevalidate ( params . request , this . renderOpts . previewProps )
. isManualRevalidate
) {
return { finished : false }
}
2022-06-14 18:50:05 +02:00
const normalizedPathname = removeTrailingSlash ( params . parsed . pathname || '' )
2022-05-19 17:46:21 +02:00
// For middleware to "fetch" we must always provide an absolute URL
2022-06-10 19:35:12 +02:00
const query = urlQueryToSearchParams ( params . parsed . query ) . toString ( )
const locale = params . parsed . query . __nextLocale
2022-06-25 02:31:23 +02:00
const url = ` ${ getRequestMeta ( params . request , '_protocol' ) } :// ${
this . hostname
} : $ { this . port } $ { locale ? ` / ${ locale } ` : '' } $ { params . parsed . pathname } $ {
query ? ` ? ${ query } ` : ''
} `
2022-06-10 19:35:12 +02:00
2022-05-19 17:46:21 +02:00
if ( ! url . startsWith ( 'http' ) ) {
throw new Error (
'To use middleware you must provide a `hostname` and `port` to the Next.js Server'
)
}
const page : { name? : string ; params ? : { [ key : string ] : string } } = { }
if ( await this . hasPage ( normalizedPathname ) ) {
page . name = params . parsedUrl . pathname
} else if ( this . dynamicRoutes ) {
for ( const dynamicRoute of this . dynamicRoutes ) {
const matchParams = dynamicRoute . match ( normalizedPathname )
if ( matchParams ) {
page . name = dynamicRoute . page
page . params = matchParams
break
}
}
}
const allHeaders = new Headers ( )
let result : FetchEventResult | null = null
const method = ( params . request . method || 'GET' ) . toUpperCase ( )
2022-07-30 23:45:58 +02:00
const middleware = this . getMiddleware ( )
if ( ! middleware ) {
return { finished : false }
}
if ( ! ( await this . hasMiddleware ( middleware . page ) ) ) {
console . warn ( ` The Edge Function for ${ middleware . page } was not found ` )
return { finished : false }
}
2022-05-19 17:46:21 +02:00
2022-07-30 23:45:58 +02:00
if ( middleware && middleware . match ( normalizedPathname ) ) {
await this . ensureMiddleware ( )
const middlewareInfo = this . getEdgeFunctionInfo ( {
page : middleware.page ,
middleware : true ,
} )
2022-05-19 17:46:21 +02:00
2022-07-30 23:45:58 +02:00
if ( ! middlewareInfo ) {
throw new MiddlewareNotFoundError ( )
}
2022-06-24 20:50:49 +02:00
2022-07-30 23:45:58 +02:00
result = await run ( {
distDir : this.distDir ,
name : middlewareInfo.name ,
paths : middlewareInfo.paths ,
env : middlewareInfo.env ,
edgeFunctionEntry : middlewareInfo ,
request : {
headers : params.request.headers ,
method ,
nextConfig : {
basePath : this.nextConfig.basePath ,
i18n : this.nextConfig.i18n ,
trailingSlash : this.nextConfig.trailingSlash ,
2022-05-19 17:46:21 +02:00
} ,
2022-07-30 23:45:58 +02:00
url : url ,
page : page ,
body : getRequestMeta ( params . request , '__NEXT_CLONABLE_BODY' ) ,
} ,
useCache : ! this . nextConfig . experimental . runtime ,
onWarning : params.onWarning ,
} )
2022-05-19 17:46:21 +02:00
2022-07-30 23:45:58 +02:00
for ( let [ key , value ] of result . response . headers ) {
if ( key !== 'x-middleware-next' ) {
allHeaders . append ( key , value )
2022-05-19 17:46:21 +02:00
}
2022-07-30 23:45:58 +02:00
}
2022-05-19 17:46:21 +02:00
2022-07-30 23:45:58 +02:00
if ( ! this . renderOpts . dev ) {
result . waitUntil . catch ( ( error ) = > {
console . error ( ` Uncaught: middleware waitUntil errored ` , error )
} )
2022-05-19 17:46:21 +02:00
}
}
if ( ! result ) {
this . render404 ( params . request , params . response , params . parsed )
2022-06-15 02:58:13 +02:00
return { finished : true }
2022-05-19 17:46:21 +02:00
} else {
for ( let [ key , value ] of allHeaders ) {
result . response . headers . set ( key , value )
}
}
return result
}
2022-06-21 21:04:48 +02:00
protected generateCatchAllMiddlewareRoute ( devReady? : boolean ) : Route [ ] {
if ( this . minimalMode ) return [ ]
2022-08-15 20:20:28 +02:00
const routes = [ ]
if ( ! this . renderOpts . dev || devReady ) {
if ( this . getMiddleware ( ) ) {
const middlewareCatchAllRoute : Route = {
match : getPathMatch ( '/:path*' ) ,
matchesBasePath : true ,
matchesLocale : true ,
type : 'route' ,
name : 'middleware catchall' ,
fn : async ( req , res , _params , parsed ) = > {
const middleware = this . getMiddleware ( )
if ( ! middleware ) {
return { finished : false }
}
2022-06-21 21:04:48 +02:00
2022-08-15 20:20:28 +02:00
const initUrl = getRequestMeta ( req , '__NEXT_INIT_URL' ) !
const parsedUrl = parseUrl ( initUrl )
const pathnameInfo = getNextPathnameInfo ( parsedUrl . pathname , {
nextConfig : this.nextConfig ,
} )
2022-03-28 23:16:43 +02:00
2022-08-15 20:20:28 +02:00
parsedUrl . pathname = pathnameInfo . pathname
const normalizedPathname = removeTrailingSlash (
parsed . pathname || ''
)
if ( ! middleware . match ( normalizedPathname ) ) {
return { finished : false }
}
2022-01-19 13:36:06 +01:00
2022-08-15 20:20:28 +02:00
let result : Awaited <
ReturnType < typeof NextNodeServer.prototype.runMiddleware >
>
2022-01-19 13:36:06 +01:00
2022-08-15 20:20:28 +02:00
try {
result = await this . runMiddleware ( {
request : req ,
response : res ,
parsedUrl : parsedUrl ,
parsed : parsed ,
} )
} catch ( err ) {
if ( isError ( err ) && err . code === 'ENOENT' ) {
await this . render404 ( req , res , parsed )
return { finished : true }
}
2022-04-06 16:35:52 +02:00
2022-08-15 20:20:28 +02:00
if ( err instanceof DecodeError ) {
res . statusCode = 400
this . renderError ( err , req , res , parsed . pathname || '' )
return { finished : true }
}
2022-01-19 13:36:06 +01:00
2022-08-15 20:20:28 +02:00
const error = getProperError ( err )
console . error ( error )
res . statusCode = 500
this . renderError ( error , req , res , parsed . pathname || '' )
return { finished : true }
}
2022-05-22 05:25:51 +02:00
2022-08-15 20:20:28 +02:00
if ( 'finished' in result ) {
return result
}
2022-01-19 13:36:06 +01:00
2022-08-15 20:20:28 +02:00
if ( result . response . headers . has ( 'x-middleware-rewrite' ) ) {
const value = result . response . headers . get ( 'x-middleware-rewrite' ) !
const rel = relativizeURL ( value , initUrl )
result . response . headers . set ( 'x-middleware-rewrite' , rel )
}
2022-01-19 13:36:06 +01:00
2022-08-15 20:20:28 +02:00
if ( result . response . headers . has ( 'Location' ) ) {
const value = result . response . headers . get ( 'Location' ) !
const rel = relativizeURL ( value , initUrl )
result . response . headers . set ( 'Location' , rel )
}
2022-01-19 13:36:06 +01:00
2022-08-15 20:20:28 +02:00
if (
! result . response . headers . has ( 'x-middleware-rewrite' ) &&
! result . response . headers . has ( 'x-middleware-next' ) &&
! result . response . headers . has ( 'Location' )
) {
result . response . headers . set ( 'x-middleware-refresh' , '1' )
}
2022-01-19 13:36:06 +01:00
2022-08-15 20:20:28 +02:00
result . response . headers . delete ( 'x-middleware-next' )
for ( const [ key , value ] of Object . entries (
toNodeHeaders ( result . response . headers )
) ) {
if (
[
'x-middleware-rewrite' ,
'x-middleware-redirect' ,
'x-middleware-refresh' ,
] . includes ( key )
) {
continue
}
if ( key !== 'content-encoding' && value !== undefined ) {
res . setHeader ( key , value )
}
}
2022-01-19 13:36:06 +01:00
2022-08-15 20:20:28 +02:00
res . statusCode = result . response . status
res . statusMessage = result . response . statusText
2022-01-19 13:36:06 +01:00
2022-08-15 20:20:28 +02:00
const location = result . response . headers . get ( 'Location' )
if ( location ) {
res . statusCode = result . response . status
if ( res . statusCode === 308 ) {
res . setHeader ( 'Refresh' , ` 0;url= ${ location } ` )
}
2022-01-19 13:36:06 +01:00
2022-08-15 20:20:28 +02:00
res . body ( location ) . send ( )
return {
finished : true ,
}
}
2022-01-27 23:06:39 +01:00
2022-08-15 20:20:28 +02:00
if ( result . response . headers . has ( 'x-middleware-rewrite' ) ) {
const rewritePath = result . response . headers . get (
'x-middleware-rewrite'
) !
const parsedDestination = parseUrl ( rewritePath )
const newUrl = parsedDestination . pathname
if (
parsedDestination . protocol &&
( parsedDestination . port
? ` ${ parsedDestination . hostname } : ${ parsedDestination . port } `
: parsedDestination . hostname ) !== req . headers . host
) {
return this . proxyRequest (
req as NodeNextRequest ,
res as NodeNextResponse ,
parsedDestination
)
}
2022-01-19 13:36:06 +01:00
2022-08-15 20:20:28 +02:00
if ( this . nextConfig . i18n ) {
const localePathResult = normalizeLocalePath (
newUrl ,
this . nextConfig . i18n . locales
)
if ( localePathResult . detectedLocale ) {
parsedDestination . query . __nextLocale =
localePathResult . detectedLocale
}
}
2022-01-19 13:36:06 +01:00
2022-08-15 20:20:28 +02:00
addRequestMeta ( req , '_nextRewroteUrl' , newUrl )
addRequestMeta ( req , '_nextDidRewrite' , newUrl !== req . url )
2022-01-19 13:36:06 +01:00
2022-08-15 20:20:28 +02:00
return {
finished : false ,
pathname : newUrl ,
query : parsedDestination.query ,
}
2022-04-06 16:35:52 +02:00
}
2022-01-19 13:36:06 +01:00
2022-08-15 20:20:28 +02:00
if ( result . response . headers . has ( 'x-middleware-refresh' ) ) {
res . statusCode = result . response . status
for await ( const chunk of result . response . body || ( [ ] as any ) ) {
this . streamResponseChunk ( res as NodeNextResponse , chunk )
}
res . send ( )
return {
finished : true ,
}
}
2022-03-28 23:16:43 +02:00
2022-08-15 20:20:28 +02:00
return {
finished : false ,
}
} ,
2022-01-19 13:36:06 +01:00
}
2022-03-28 23:16:43 +02:00
2022-08-15 20:20:28 +02:00
routes . push ( middlewareCatchAllRoute )
}
if ( this . getEdgeFunctions ( ) . length ) {
const edgeCatchAllRoute : Route = {
match : getPathMatch ( '/:path*' ) ,
type : 'route' ,
name : 'edge functions catchall' ,
fn : async ( req , res , _params , parsed ) = > {
const edgeFunctions = this . getEdgeFunctions ( )
if ( ! edgeFunctions . length ) return { finished : false }
const { query , pathname } = parsed
const normalizedPathname = removeTrailingSlash ( pathname || '' )
let page = normalizedPathname
let params : Params | undefined = undefined
for ( const edgeFunction of edgeFunctions ) {
const matched = edgeFunction . match ( page )
if ( matched ) {
params = matched
page = edgeFunction . page
break
}
}
2022-01-19 13:36:06 +01:00
2022-08-15 20:20:28 +02:00
const edgeSSRResult = await this . runEdgeFunction ( {
req ,
res ,
query ,
params ,
page ,
} )
return {
finished : ! ! edgeSSRResult ,
}
} ,
2022-04-06 16:35:52 +02:00
}
2022-06-21 21:04:48 +02:00
2022-08-15 20:20:28 +02:00
routes . push ( edgeCatchAllRoute )
}
2022-06-21 21:04:48 +02:00
}
return routes
2022-04-06 16:35:52 +02:00
}
2022-03-28 23:16:43 +02:00
2022-01-21 16:31:47 +01:00
private _cachedPreviewManifest : PrerenderManifest | undefined
protected getPrerenderManifest ( ) : PrerenderManifest {
if ( this . _cachedPreviewManifest ) {
return this . _cachedPreviewManifest
}
const manifest = require ( join ( this . distDir , PRERENDER_MANIFEST ) )
return ( this . _cachedPreviewManifest = manifest )
}
protected getRoutesManifest() {
return require ( join ( this . distDir , ROUTES_MANIFEST ) )
}
2022-01-27 23:06:39 +01:00
2022-07-22 20:42:35 +02:00
protected attachRequestMeta (
req : BaseNextRequest ,
parsedUrl : NextUrlWithParsedQuery
) {
const protocol = (
( req as NodeNextRequest ) . originalRequest ? . socket as TLSSocket
) ? . encrypted
? 'https'
: 'http'
// When there are hostname and port we build an absolute URL
const initUrl =
this . hostname && this . port
? ` ${ protocol } :// ${ this . hostname } : ${ this . port } ${ req . url } `
: req . url
addRequestMeta ( req , '__NEXT_INIT_URL' , initUrl )
addRequestMeta ( req , '__NEXT_INIT_QUERY' , { . . . parsedUrl . query } )
addRequestMeta ( req , '_protocol' , protocol )
addRequestMeta ( req , '__NEXT_CLONABLE_BODY' , getClonableBody ( req . body ) )
}
2022-06-21 21:04:48 +02:00
protected async runEdgeFunction ( params : {
req : BaseNextRequest | NodeNextRequest
res : BaseNextResponse | NodeNextResponse
2022-06-13 20:17:44 +02:00
query : ParsedUrlQuery
2022-06-21 21:04:48 +02:00
params : Params | undefined
2022-06-13 20:17:44 +02:00
page : string
fix(edge): error handling for edge route and middleware is inconsistent (#38401)
## What’s in there?
This PR brings more consistency in how errors and warnings are reported when running code in the Edge Runtime:
- Dynamic code evaluation (`eval()`, `new Function()`, `WebAssembly.instantiate()`, `WebAssembly.compile()`…)
- Usage of Node.js global APIs (`BroadcastChannel`, `Buffer`, `TextDecoderStream`, `setImmediate()`...)
- Usage of Node.js modules (`fs`, `path`, `child_process`…)
The new error messages should mention *Edge Runtime* instead of *Middleware*, so they are valid in both cases.
It also fixes a bug where the process polyfill would issue a warning for `process.cwd` (which is `undefined` but legit). Now, one has to invoke the function `process.cwd()` to trigger the error.
It finally fixes the react-dev-overlay, where links from middleware and Edge API route files could not be opened because of the `(middleware)/` prefix in their name.
About the later, please note that we can’t easily remove the prefix or change it for Edge API routes. It comes from the Webpack layer, which is the same for both. We may consider renaming it to *edge* instead in the future.
## How to test?
These changes are almost fully covered with tests:
```bash
pnpm testheadless --testPathPattern runtime-dynamic
pnpm testheadless --testPathPattern runtime-with-node
pnpm testheadless --testPathPattern runtime-module
pnpm testheadless --testPathPattern middleware-dev-errors
```
To try them out manually, you can write a middleware and Edge route files like these:
```jsx
// middleware.js
import { NextResponse } from 'next/server'
import { basename } from 'path'
export default async function middleware() {
eval('2+2')
setImmediate(() => {})
basename()
return NextResponse.next()
}
export const config = { matcher: '/' }
```
```jsx
// pages/api/route.js
import { basename } from 'path'
export default async function handle() {
eval('2+2')
setImmediate(() => {})
basename()
return Response.json({ ok: true })
}
export const config = { runtime: 'experimental-edge' }
```
The expected behaviours are:
- [x] dev, middleware/edge route is using a node.js module: error at runtime (logs + read-dev-overlay):
```bash
error - (middleware)/pages/api/route.js (1:0) @ Object.handle [as handler]
error - The edge runtime does not support Node.js 'path' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
> 1 | import { basename } from "path";
2 | export default async function handle() {
```
- [x] build, middleware/edge route is using a node.js module: warning but succeeds
```bash
warn - Compiled with warnings
./middleware.js
A Node.js module is loaded ('path' at line 4) which is not supported in the Edge Runtime.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
./pages/api/route.js
A Node.js module is loaded ('path' at line 1) which is not supported in the Edge Runtime.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
```
- [x] production, middleware/edge route is using a node.js module: error at runtime (logs + 500 error)
```bash
Error: The edge runtime does not support Node.js 'path' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
at <unknown> (file:///Users/damien/dev/next.js/packages/next/server/web/sandbox/context.ts:149)
```
- [x] dev, middleware/edge route is using a node.js global API: error at runtime (logs + read-dev-overlay):
```bash
error - (middleware)/pages/api/route.js (4:2) @ Object.handle [as handler]
error - A Node.js API is used (setImmediate) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
2 |
3 | export default async function handle() {
> 4 | setImmediate(() => {})
| ^
```
- [x] build, middleware/edge route is using a node.js global API: warning but succeeds
```bash
warn - Compiled with warnings
./middleware.js
A Node.js API is used (setImmediate at line: 6) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
./pages/api/route.js
A Node.js API is used (setImmediate at line: 3) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
```
- [x] production, middleware/edge route is using a node.js module: error at runtime (logs + 500 error)
```bash
Error: A Node.js API is used (setImmediate) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
at <unknown> (file:///Users/damien/dev/next.js/packages/next/server/web/sandbox/context.ts:330)
```
- [x] dev, middleware/edge route is loading dynamic code: warning at runtime (logs + read-dev-overlay) and request succeeds (we allow dynamic code in dev only):
```bash
warn - (middleware)/middleware.js (7:2) @ Object.middleware [as handler]
warn - Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime
5 |
6 | export default async function middleware() {
> 7 | eval('2+2')
```
- [x] build, middleware/edge route is loading dynamic code: build fails with error:
```bash
Failed to compile.
./middleware.js
Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime
Used by default
./pages/api/route.js
Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime
Used by default
```
## Notes to reviewers
Edge-related errors are either issued from `next/server/web/sandbox/context.ts` file (runtime errors) or from `next/build/webpack/plugins/middleware-plugin.ts` (webpack compilation).
The previous implementation (I’m pleading guilty here) was way too verbose: some errors (Node.js global APIs like using `process.cwd()`) could be reported several times, and the previous mechanism to dedupe them (in middleware-plugin) wasn’t really effective.
Changes in tests are due to renaming existing tests such as `test/integration/middleware-with-node.js-apis` into `test/integration/edge-runtime-with-node.js-apis`. I extended them to cover Edge API route.
@hanneslund I’ve pushed the improvement you did in https://github.com/vercel/next.js/pull/38289/ one step further to avoid duplication.
2022-07-21 16:53:23 +02:00
onWarning ? : ( warning : Error ) = > void
2022-06-21 21:04:48 +02:00
} ) : Promise < FetchEventResult | null > {
2022-06-13 20:17:44 +02:00
let middlewareInfo : ReturnType < typeof this.getEdgeFunctionInfo > | undefined
try {
2022-07-30 23:45:58 +02:00
await this . ensureEdgeFunction ( params . page )
2022-06-13 20:17:44 +02:00
middlewareInfo = this . getEdgeFunctionInfo ( {
page : params.page ,
middleware : false ,
} )
} catch {
2022-06-21 21:04:48 +02:00
return null
2022-06-13 20:17:44 +02:00
}
2022-06-24 20:50:49 +02:00
if ( ! middlewareInfo ) {
return null
}
2022-06-13 20:17:44 +02:00
// For middleware to "fetch" we must always provide an absolute URL
const url = getRequestMeta ( params . req , '__NEXT_INIT_URL' ) !
if ( ! url . startsWith ( 'http' ) ) {
throw new Error (
'To use middleware you must provide a `hostname` and `port` to the Next.js Server'
)
}
const result = await run ( {
2022-07-19 19:27:15 +02:00
distDir : this.distDir ,
2022-06-13 20:17:44 +02:00
name : middlewareInfo.name ,
paths : middlewareInfo.paths ,
env : middlewareInfo.env ,
2022-07-19 19:27:15 +02:00
edgeFunctionEntry : middlewareInfo ,
2022-06-13 20:17:44 +02:00
request : {
headers : params.req.headers ,
method : params.req.method ,
nextConfig : {
basePath : this.nextConfig.basePath ,
i18n : this.nextConfig.i18n ,
trailingSlash : this.nextConfig.trailingSlash ,
} ,
url ,
page : {
name : params.page ,
. . . ( params . params && { params : params.params } ) ,
} ,
2022-07-21 20:29:19 +02:00
body : getRequestMeta ( params . req , '__NEXT_CLONABLE_BODY' ) ,
2022-06-13 20:17:44 +02:00
} ,
useCache : ! this . nextConfig . experimental . runtime ,
fix(edge): error handling for edge route and middleware is inconsistent (#38401)
## What’s in there?
This PR brings more consistency in how errors and warnings are reported when running code in the Edge Runtime:
- Dynamic code evaluation (`eval()`, `new Function()`, `WebAssembly.instantiate()`, `WebAssembly.compile()`…)
- Usage of Node.js global APIs (`BroadcastChannel`, `Buffer`, `TextDecoderStream`, `setImmediate()`...)
- Usage of Node.js modules (`fs`, `path`, `child_process`…)
The new error messages should mention *Edge Runtime* instead of *Middleware*, so they are valid in both cases.
It also fixes a bug where the process polyfill would issue a warning for `process.cwd` (which is `undefined` but legit). Now, one has to invoke the function `process.cwd()` to trigger the error.
It finally fixes the react-dev-overlay, where links from middleware and Edge API route files could not be opened because of the `(middleware)/` prefix in their name.
About the later, please note that we can’t easily remove the prefix or change it for Edge API routes. It comes from the Webpack layer, which is the same for both. We may consider renaming it to *edge* instead in the future.
## How to test?
These changes are almost fully covered with tests:
```bash
pnpm testheadless --testPathPattern runtime-dynamic
pnpm testheadless --testPathPattern runtime-with-node
pnpm testheadless --testPathPattern runtime-module
pnpm testheadless --testPathPattern middleware-dev-errors
```
To try them out manually, you can write a middleware and Edge route files like these:
```jsx
// middleware.js
import { NextResponse } from 'next/server'
import { basename } from 'path'
export default async function middleware() {
eval('2+2')
setImmediate(() => {})
basename()
return NextResponse.next()
}
export const config = { matcher: '/' }
```
```jsx
// pages/api/route.js
import { basename } from 'path'
export default async function handle() {
eval('2+2')
setImmediate(() => {})
basename()
return Response.json({ ok: true })
}
export const config = { runtime: 'experimental-edge' }
```
The expected behaviours are:
- [x] dev, middleware/edge route is using a node.js module: error at runtime (logs + read-dev-overlay):
```bash
error - (middleware)/pages/api/route.js (1:0) @ Object.handle [as handler]
error - The edge runtime does not support Node.js 'path' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
> 1 | import { basename } from "path";
2 | export default async function handle() {
```
- [x] build, middleware/edge route is using a node.js module: warning but succeeds
```bash
warn - Compiled with warnings
./middleware.js
A Node.js module is loaded ('path' at line 4) which is not supported in the Edge Runtime.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
./pages/api/route.js
A Node.js module is loaded ('path' at line 1) which is not supported in the Edge Runtime.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
```
- [x] production, middleware/edge route is using a node.js module: error at runtime (logs + 500 error)
```bash
Error: The edge runtime does not support Node.js 'path' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
at <unknown> (file:///Users/damien/dev/next.js/packages/next/server/web/sandbox/context.ts:149)
```
- [x] dev, middleware/edge route is using a node.js global API: error at runtime (logs + read-dev-overlay):
```bash
error - (middleware)/pages/api/route.js (4:2) @ Object.handle [as handler]
error - A Node.js API is used (setImmediate) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
2 |
3 | export default async function handle() {
> 4 | setImmediate(() => {})
| ^
```
- [x] build, middleware/edge route is using a node.js global API: warning but succeeds
```bash
warn - Compiled with warnings
./middleware.js
A Node.js API is used (setImmediate at line: 6) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
./pages/api/route.js
A Node.js API is used (setImmediate at line: 3) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
```
- [x] production, middleware/edge route is using a node.js module: error at runtime (logs + 500 error)
```bash
Error: A Node.js API is used (setImmediate) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
at <unknown> (file:///Users/damien/dev/next.js/packages/next/server/web/sandbox/context.ts:330)
```
- [x] dev, middleware/edge route is loading dynamic code: warning at runtime (logs + read-dev-overlay) and request succeeds (we allow dynamic code in dev only):
```bash
warn - (middleware)/middleware.js (7:2) @ Object.middleware [as handler]
warn - Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime
5 |
6 | export default async function middleware() {
> 7 | eval('2+2')
```
- [x] build, middleware/edge route is loading dynamic code: build fails with error:
```bash
Failed to compile.
./middleware.js
Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime
Used by default
./pages/api/route.js
Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime
Used by default
```
## Notes to reviewers
Edge-related errors are either issued from `next/server/web/sandbox/context.ts` file (runtime errors) or from `next/build/webpack/plugins/middleware-plugin.ts` (webpack compilation).
The previous implementation (I’m pleading guilty here) was way too verbose: some errors (Node.js global APIs like using `process.cwd()`) could be reported several times, and the previous mechanism to dedupe them (in middleware-plugin) wasn’t really effective.
Changes in tests are due to renaming existing tests such as `test/integration/middleware-with-node.js-apis` into `test/integration/edge-runtime-with-node.js-apis`. I extended them to cover Edge API route.
@hanneslund I’ve pushed the improvement you did in https://github.com/vercel/next.js/pull/38289/ one step further to avoid duplication.
2022-07-21 16:53:23 +02:00
onWarning : params.onWarning ,
2022-06-13 20:17:44 +02:00
} )
params . res . statusCode = result . response . status
params . res . statusMessage = result . response . statusText
result . response . headers . forEach ( ( value , key ) = > {
params . res . appendHeader ( key , value )
} )
if ( result . response . body ) {
// TODO(gal): not sure that we always need to stream
bodyStreamToNodeStream ( result . response . body ) . pipe (
2022-06-21 21:04:48 +02:00
( params . res as NodeNextResponse ) . originalResponse
2022-06-13 20:17:44 +02:00
)
} else {
2022-06-21 21:04:48 +02:00
; ( params . res as NodeNextResponse ) . originalResponse . end ( )
2022-06-13 20:17:44 +02:00
}
2022-06-21 21:04:48 +02:00
return result
2022-06-13 20:17:44 +02:00
}
2022-08-15 20:58:49 +02:00
protected get _isLikeServerless ( ) : boolean {
return isTargetLikeServerless ( this . nextConfig . target )
}
protected get serverDistDir() {
return join (
this . distDir ,
this . _isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
)
}
2021-12-07 02:14:55 +01:00
}