diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/api-handler.ts b/packages/next/build/webpack/loaders/next-serverless-loader/api-handler.ts index 6171c86a01f59..0d85e3731d89b 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/api-handler.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/api-handler.ts @@ -3,6 +3,7 @@ import { IncomingMessage, ServerResponse } from 'http' import { apiResolver } from '../../../../server/api-utils' import { getUtils, vercelHeader, ServerlessHandlerCtx } from './utils' import { DecodeError } from '../../../../shared/lib/utils' +import { NodeNextResponse, NodeNextRequest } from '../../../../server/base-http' export function getApiHandler(ctx: ServerlessHandlerCtx) { const { pageModule, encodedPreviewProps, pageIsDynamic } = ctx @@ -13,7 +14,15 @@ export function getApiHandler(ctx: ServerlessHandlerCtx) { normalizeDynamicRouteParams, } = getUtils(ctx) - return async (req: IncomingMessage, res: ServerResponse) => { + return async ( + rawReq: NodeNextRequest | IncomingMessage, + rawRes: NodeNextResponse | ServerResponse + ) => { + const req = + rawReq instanceof IncomingMessage ? new NodeNextRequest(rawReq) : rawReq + const res = + rawRes instanceof ServerResponse ? new NodeNextResponse(rawRes) : rawRes + try { // We need to trust the dynamic route params from the proxy // to ensure we are using the correct values @@ -41,8 +50,8 @@ export function getApiHandler(ctx: ServerlessHandlerCtx) { } await apiResolver( - req, - res, + req.originalRequest, + res.originalResponse, Object.assign({}, parsedUrl.query, params), await pageModule, encodedPreviewProps, @@ -53,7 +62,7 @@ export function getApiHandler(ctx: ServerlessHandlerCtx) { if (err instanceof DecodeError) { res.statusCode = 400 - res.end('Bad Request') + res.body('Bad Request').send() } else { // Throw the error to crash the serverless function throw err diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts index 92adf265a2788..28b1ce0953142 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts @@ -25,6 +25,7 @@ import cookie from 'next/dist/compiled/cookie' import { TEMPORARY_REDIRECT_STATUS } from '../../../../shared/lib/constants' import { NextConfig } from '../../../../server/config' import { addRequestMeta } from '../../../../server/request-meta' +import { BaseNextRequest } from '../../../../server/base-http' const getCustomRouteMatcher = pathMatch(true) @@ -85,7 +86,10 @@ export function getUtils({ defaultRouteMatches = dynamicRouteMatcher(page) as ParsedUrlQuery } - function handleRewrites(req: IncomingMessage, parsedUrl: UrlWithParsedQuery) { + function handleRewrites( + req: BaseNextRequest | IncomingMessage, + parsedUrl: UrlWithParsedQuery + ) { for (const rewrite of rewrites) { const matcher = getCustomRouteMatcher(rewrite.source) let params = matcher(parsedUrl.pathname) @@ -150,7 +154,10 @@ export function getUtils({ return parsedUrl } - function handleBasePath(req: IncomingMessage, parsedUrl: UrlWithParsedQuery) { + function handleBasePath( + req: BaseNextRequest | IncomingMessage, + parsedUrl: UrlWithParsedQuery + ) { // always strip the basePath if configured since it is required req.url = req.url!.replace(new RegExp(`^${basePath}`), '') || '/' parsedUrl.pathname = @@ -158,7 +165,7 @@ export function getUtils({ } function getParamsFromRouteMatches( - req: IncomingMessage, + req: BaseNextRequest | IncomingMessage, renderOpts?: any, detectedLocale?: string ) { @@ -269,7 +276,10 @@ export function getUtils({ return pathname } - function normalizeVercelUrl(req: IncomingMessage, trustQuery: boolean) { + function normalizeVercelUrl( + req: BaseNextRequest | IncomingMessage, + trustQuery: boolean + ) { // make sure to normalize req.url on Vercel to strip dynamic params // from the query which are added during routing if (pageIsDynamic && trustQuery && defaultRouteRegex) { @@ -374,7 +384,7 @@ export function getUtils({ if (detectedDomain) { defaultLocale = detectedDomain.defaultLocale detectedLocale = defaultLocale - addRequestMeta(req, '__nextIsLocaleDomain', true) + addRequestMeta(req as any, '__nextIsLocaleDomain', true) } // if not domain specific locale use accept-language preferred @@ -394,7 +404,7 @@ export function getUtils({ ...parsedUrl, pathname: localePathResult.pathname, }) - addRequestMeta(req, '__nextStrippedLocale', true) + addRequestMeta(req as any, '__nextStrippedLocale', true) parsedUrl.pathname = localePathResult.pathname } diff --git a/packages/next/server/api-utils.ts b/packages/next/server/api-utils.ts index 8fb0e6f2e3f2e..fab3277976969 100644 --- a/packages/next/server/api-utils.ts +++ b/packages/next/server/api-utils.ts @@ -10,6 +10,7 @@ import { sendEtagResponse } from './send-payload' import generateETag from 'next/dist/compiled/etag' import isError from '../lib/is-error' import { interopDefault } from '../lib/interop-default' +import { BaseNextRequest, BaseNextResponse } from './base-http' export type NextApiRequestCookies = { [key: string]: string } export type NextApiRequestQuery = { [key: string]: string | string[] } @@ -141,7 +142,7 @@ export async function apiResolver( * @param req request object */ export async function parseBody( - req: NextApiRequest, + req: IncomingMessage, limit: string | number ): Promise { let contentType @@ -335,11 +336,11 @@ const COOKIE_NAME_PRERENDER_BYPASS = `__prerender_bypass` const COOKIE_NAME_PRERENDER_DATA = `__next_preview_data` export const SYMBOL_PREVIEW_DATA = Symbol(COOKIE_NAME_PRERENDER_DATA) -const SYMBOL_CLEARED_COOKIES = Symbol(COOKIE_NAME_PRERENDER_BYPASS) +export const SYMBOL_CLEARED_COOKIES = Symbol(COOKIE_NAME_PRERENDER_BYPASS) export function tryGetPreviewData( - req: IncomingMessage, - res: ServerResponse, + req: IncomingMessage | BaseNextRequest, + res: ServerResponse | BaseNextResponse, options: __ApiPreviewProps ): PreviewData { // Read cached preview data if present diff --git a/packages/next/server/base-http.ts b/packages/next/server/base-http.ts new file mode 100644 index 0000000000000..b353df2d09683 --- /dev/null +++ b/packages/next/server/base-http.ts @@ -0,0 +1,288 @@ +import type { ServerResponse, IncomingMessage, IncomingHttpHeaders } from 'http' +import type { Writable, Readable } from 'stream' +import { PERMANENT_REDIRECT_STATUS } from '../shared/lib/constants' +import { + getCookieParser, + NextApiRequestCookies, + parseBody, + SYMBOL_CLEARED_COOKIES, +} from './api-utils' +import { I18NConfig } from './config-shared' +import { NEXT_REQUEST_META, RequestMeta } from './request-meta' + +export interface BaseNextRequestConfig { + basePath: string | undefined + i18n?: I18NConfig + trailingSlash?: boolean | undefined +} + +export abstract class BaseNextRequest { + protected _cookies: NextApiRequestCookies | undefined + public abstract headers: IncomingHttpHeaders + + constructor(public method: string, public url: string, public body: Body) {} + + abstract parseBody(limit: string | number): Promise + + // Utils implemented using the abstract methods above + + public get cookies() { + if (this._cookies) return this._cookies + return (this._cookies = getCookieParser(this.headers)()) + } +} + +export class NodeNextRequest extends BaseNextRequest { + public headers = this._req.headers; + + [NEXT_REQUEST_META]: RequestMeta + + get originalRequest() { + // Need to mimic these changes to the original req object for places where we use it: + // render.tsx, api/ssg requests + this._req[NEXT_REQUEST_META] = this[NEXT_REQUEST_META] + this._req.url = this.url + this._req.cookies = this.cookies + return this._req + } + + constructor( + private _req: IncomingMessage & { + [NEXT_REQUEST_META]?: RequestMeta + cookies?: NextApiRequestCookies + } + ) { + super(_req.method!.toUpperCase(), _req.url!, _req) + } + + async parseBody(limit: string | number): Promise { + return parseBody(this._req, limit) + } +} + +export class WebNextRequest extends BaseNextRequest { + public request: Request + public headers: IncomingHttpHeaders + + constructor(request: Request) { + const url = new URL(request.url) + + super( + request.method, + url.href.slice(url.origin.length), + request.clone().body + ) + this.request = request + + this.headers = {} + for (const [name, value] of request.headers.entries()) { + this.headers[name] = value + } + } + + async parseBody(_limit: string | number): Promise { + throw new Error('parseBody is not implemented in the web runtime') + } +} + +export abstract class BaseNextResponse { + abstract statusCode: number | undefined + abstract statusMessage: string | undefined + abstract get sent(): boolean + + constructor(public destination: Destination) {} + + /** + * Sets a value for the header overwriting existing values + */ + abstract setHeader(name: string, value: string | string[]): this + + /** + * Appends value for the given header name + */ + abstract appendHeader(name: string, value: string): this + + /** + * Get all vaues for a header as an array or undefined if no value is present + */ + abstract getHeaderValues(name: string): string[] | undefined + + abstract hasHeader(name: string): boolean + + /** + * Get vaues for a header concatenated using `,` or undefined if no value is present + */ + abstract getHeader(name: string): string | undefined + + abstract body(value: string): this + + abstract send(): void + + // Utils implemented using the abstract methods above + + redirect(destination: string, statusCode: number) { + this.setHeader('Location', destination) + this.statusCode = statusCode + + // Since IE11 doesn't support the 308 header add backwards + // compatibility using refresh header + if (statusCode === PERMANENT_REDIRECT_STATUS) { + this.setHeader('Refresh', `0;url=${destination}`) + } + return this + } +} + +export class NodeNextResponse extends BaseNextResponse { + private textBody: string | undefined = undefined + + public [SYMBOL_CLEARED_COOKIES]?: boolean + + get originalResponse() { + if (SYMBOL_CLEARED_COOKIES in this) { + this._res[SYMBOL_CLEARED_COOKIES] = this[SYMBOL_CLEARED_COOKIES] + } + + return this._res + } + + constructor( + private _res: ServerResponse & { [SYMBOL_CLEARED_COOKIES]?: boolean } + ) { + super(_res) + } + + get sent() { + return this._res.finished || this._res.headersSent + } + + get statusCode() { + return this._res.statusCode + } + + set statusCode(value: number) { + this._res.statusCode = value + } + + get statusMessage() { + return this._res.statusMessage + } + + set statusMessage(value: string) { + this._res.statusMessage = value + } + + setHeader(name: string, value: string | string[]): this { + this._res.setHeader(name, value) + return this + } + + getHeaderValues(name: string): string[] | undefined { + const values = this._res.getHeader(name) + + if (values === undefined) return undefined + + return (Array.isArray(values) ? values : [values]).map((value) => + value.toString() + ) + } + + hasHeader(name: string): boolean { + return this._res.hasHeader(name) + } + + getHeader(name: string): string | undefined { + const values = this.getHeaderValues(name) + return Array.isArray(values) ? values.join(',') : undefined + } + + appendHeader(name: string, value: string): this { + const currentValues = this.getHeaderValues(name) ?? [] + + if (!currentValues.includes(value)) { + this._res.setHeader(name, [...currentValues, value]) + } + + return this + } + + body(value: string) { + this.textBody = value + return this + } + + send() { + this._res.end(this.textBody) + } +} + +export class WebNextResponse extends BaseNextResponse { + private headers = new Headers() + private textBody: string | undefined = undefined + private _sent = false + + private sendPromise = new Promise((resolve) => { + this.sendResolve = resolve + }) + private sendResolve?: () => void + private response = this.sendPromise.then(() => { + return new Response(this.textBody ?? this.transformStream.readable, { + headers: this.headers, + status: this.statusCode, + statusText: this.statusMessage, + }) + }) + + public statusCode: number | undefined + public statusMessage: string | undefined + + get sent() { + return this._sent + } + + constructor(public transformStream = new TransformStream()) { + super(transformStream.writable) + } + + setHeader(name: string, value: string | string[]): this { + this.headers.delete(name) + for (const val of Array.isArray(value) ? value : [value]) { + this.headers.append(name, val) + } + return this + } + + getHeaderValues(name: string): string[] | undefined { + // https://developer.mozilla.org/en-US/docs/Web/API/Headers/get#example + return this.getHeader(name) + ?.split(',') + .map((v) => v.trimStart()) + } + + getHeader(name: string): string | undefined { + return this.headers.get(name) ?? undefined + } + + hasHeader(name: string): boolean { + return this.headers.has(name) + } + + appendHeader(name: string, value: string): this { + this.headers.append(name, value) + return this + } + + body(value: string) { + this.textBody = value + return this + } + + send() { + this.sendResolve?.() + this._sent = true + } + + toResponse() { + return this.response + } +} diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 15310b11f99e9..9ff580b0ad105 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -4,10 +4,8 @@ import type { DomainLocale } from './config' import type { DynamicRoutes, PageChecker, Params, Route } from './router' import type { FetchEventResult } from './web/types' import type { FontManifest } from './font-utils' -import type { IncomingMessage, ServerResponse } from 'http' import type { LoadComponentsReturnType } from './load-components' import type { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin' -import type { NextApiRequest, NextApiResponse } from '../shared/lib/utils' import type { NextConfig, NextConfigComplete } from './config-shared' import type { NextParsedUrlQuery, NextUrlWithParsedQuery } from './request-meta' import type { ParsedNextUrl } from '../shared/lib/router/utils/parse-next-url' @@ -20,8 +18,6 @@ import type { ResponseCacheEntry, ResponseCacheValue } from './response-cache' import type { UrlWithParsedQuery } from 'url' import type { CacheFs } from '../shared/lib/utils' -import compression from 'next/dist/compiled/compression' -import Proxy from 'next/dist/compiled/http-proxy' import { join, relative, resolve, sep } from 'path' import { parse as parseQs, stringify as stringifyQs } from 'querystring' import { format as formatUrl, parse as parseUrl } from 'url' @@ -30,7 +26,6 @@ import { CLIENT_PUBLIC_FILES_PATH, CLIENT_STATIC_FILES_PATH, CLIENT_STATIC_FILES_RUNTIME, - PERMANENT_REDIRECT_STATUS, PRERENDER_MANIFEST, ROUTES_MANIFEST, SERVERLESS_DIRECTORY, @@ -47,27 +42,16 @@ import { getMiddlewareRegex, } from '../shared/lib/router/utils' import * as envConfig from '../shared/lib/runtime-config' -import { - DecodeError, - isResSent, - normalizeRepeatedSlashes, -} from '../shared/lib/utils' -import { - apiResolver, - setLazyProp, - getCookieParser, - tryGetPreviewData, -} from './api-utils' +import { DecodeError, normalizeRepeatedSlashes } from '../shared/lib/utils' +import { setLazyProp, getCookieParser, tryGetPreviewData } from './api-utils' import { isTargetLikeServerless } from './config' import pathMatch from '../shared/lib/router/utils/path-match' -import { renderToHTML } from './render' import Router, { replaceBasePath, route } from './router' import { compileNonPath, prepareDestination, } from '../shared/lib/router/utils/prepare-destination' -import { sendRenderResult, setRevalidateHeaders } from './send-payload' -import { serveStatic } from './serve-static' +import { PayloadOptions, setRevalidateHeaders } from './send-payload' import { IncrementalCache } from './incremental-cache' import { execOnce } from '../shared/lib/utils' import { isBlockedPage, isBot } from './utils' @@ -91,16 +75,11 @@ import { MIDDLEWARE_ROUTE } from '../lib/constants' import { run } from './web/sandbox' import { addRequestMeta, getRequestMeta } from './request-meta' import { toNodeHeaders } from './web/utils' +import { BaseNextRequest, BaseNextResponse } from './base-http' import { relativizeURL } from '../shared/lib/router/utils/relativize-url' const getCustomRouteMatcher = pathMatch(true) -type ExpressMiddleware = ( - req: IncomingMessage, - res: ServerResponse, - next: (err?: Error) => void -) => void - export type FindComponentsResult = { components: LoadComponentsReturnType query: NextParsedUrlQuery @@ -147,17 +126,17 @@ export interface Options { port?: number } -export interface RequestHandler { +export interface BaseRequestHandler { ( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, parsedUrl?: NextUrlWithParsedQuery | undefined ): Promise } type RequestContext = { - req: IncomingMessage - res: ServerResponse + req: BaseNextRequest + res: BaseNextResponse pathname: string query: NextParsedUrlQuery renderOpts: RenderOptsPartial @@ -202,7 +181,6 @@ export default abstract class Server { serverComponents?: boolean crossOrigin?: string } - private compression?: ExpressMiddleware private incrementalCache: IncrementalCache private responseCache: ResponseCache protected router: Router @@ -233,6 +211,63 @@ export default abstract class Server { protected abstract getPagePath(pathname: string, locales?: string[]): string protected abstract getFontManifest(): FontManifest | undefined + protected abstract sendRenderResult( + req: BaseNextRequest, + res: BaseNextResponse, + options: { + result: RenderResult + type: 'html' | 'json' + generateEtags: boolean + poweredByHeader: boolean + options?: PayloadOptions + } + ): Promise + + protected abstract sendStatic( + req: BaseNextRequest, + res: BaseNextResponse, + path: string + ): Promise + + protected abstract runApi( + req: BaseNextRequest, + res: BaseNextResponse, + query: ParsedUrlQuery, + params: Params | boolean, + page: string, + builtPagePath: string + ): Promise + + protected abstract renderHTML( + req: BaseNextRequest, + res: BaseNextResponse, + pathname: string, + query: NextParsedUrlQuery, + renderOpts: RenderOpts + ): Promise + + protected abstract streamResponseChunk( + res: BaseNextResponse, + chunk: any + ): void + + protected abstract handleCompression( + req: BaseNextRequest, + res: BaseNextResponse + ): void + + protected abstract proxyRequest( + req: BaseNextRequest, + res: BaseNextResponse, + parsedUrl: ParsedUrl + ): Promise<{ finished: boolean }> + + protected abstract imageOptimizer( + req: BaseNextRequest, + res: BaseNextResponse, + parsedUrl: UrlWithParsedQuery + ): Promise<{ finished: boolean }> + public constructor({ dir = '.', quiet = false, @@ -264,7 +299,6 @@ export default abstract class Server { publicRuntimeConfig, assetPrefix, generateEtags, - compress, } = this.nextConfig this.buildId = this.getBuildId() @@ -304,10 +338,6 @@ export default abstract class Server { this.renderOpts.runtimeConfig = publicRuntimeConfig } - if (compress && this.nextConfig.target === 'server') { - this.compression = compression() as ExpressMiddleware - } - // Initialize next/config with the environment configuration envConfig.setConfig({ serverRuntimeConfig, @@ -364,8 +394,8 @@ export default abstract class Server { } private async handleRequest( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, parsedUrl?: NextUrlWithParsedQuery ): Promise { try { @@ -374,10 +404,7 @@ export default abstract class Server { if (urlNoQuery?.match(/(\\|\/\/)/)) { const cleanUrl = normalizeRepeatedSlashes(req.url!) - res.setHeader('Location', cleanUrl) - res.setHeader('Refresh', `0;url=${cleanUrl}`) - res.statusCode = 308 - res.end(cleanUrl) + res.redirect(cleanUrl, 308).body(cleanUrl).send() return } @@ -559,9 +586,10 @@ export default abstract class Server { } if (url.locale?.redirect) { - res.setHeader('Location', url.locale.redirect) - res.statusCode = TEMPORARY_REDIRECT_STATUS - res.end(url.locale.redirect) + res + .redirect(url.locale.redirect, TEMPORARY_REDIRECT_STATUS) + .body(url.locale.redirect) + .send() return } @@ -581,11 +609,11 @@ export default abstract class Server { } this.logError(getProperError(err)) res.statusCode = 500 - res.end('Internal Server Error') + res.body('Internal Server Error').send() } } - public getRequestHandler(): RequestHandler { + public getRequestHandler(): BaseRequestHandler { return this.handleRequest.bind(this) } @@ -599,7 +627,7 @@ export default abstract class Server { // Backwards compatibility protected async close(): Promise {} - protected setImmutableAssetCacheControl(res: ServerResponse): void { + protected setImmutableAssetCacheControl(res: BaseNextResponse): void { res.setHeader('Cache-Control', 'public, max-age=31536000, immutable') } @@ -685,8 +713,8 @@ export default abstract class Server { }) protected async runMiddleware(params: { - request: IncomingMessage - response: ServerResponse + request: BaseNextRequest + response: BaseNextResponse parsedUrl: ParsedNextUrl parsed: UrlWithParsedQuery onWarning?: (warning: Error) => void @@ -1009,67 +1037,6 @@ export default abstract class Server { } as Route }) - // since initial query values are decoded by querystring.parse - // we need to re-encode them here but still allow passing through - // values from rewrites/redirects - const stringifyQuery = (req: IncomingMessage, query: ParsedUrlQuery) => { - const initialQueryValues = Object.values( - getRequestMeta(req, '__NEXT_INIT_QUERY') || {} - ) - - return stringifyQs(query, undefined, undefined, { - encodeURIComponent(value) { - if (initialQueryValues.some((val) => val === value)) { - return encodeURIComponent(value) - } - return value - }, - }) - } - - const proxyRequest = async ( - req: IncomingMessage, - res: ServerResponse, - parsedUrl: ParsedUrl - ) => { - const { query } = parsedUrl - delete (parsedUrl as any).query - parsedUrl.search = stringifyQuery(req, query) - - const target = formatUrl(parsedUrl) - const proxy = new Proxy({ - target, - changeOrigin: true, - ignorePath: true, - xfwd: true, - proxyTimeout: 30_000, // limit proxying to 30 seconds - }) - - await new Promise((proxyResolve, proxyReject) => { - let finished = false - - proxy.on('proxyReq', (proxyReq) => { - proxyReq.on('close', () => { - if (!finished) { - finished = true - proxyResolve(true) - } - }) - }) - proxy.on('error', (err) => { - if (!finished) { - finished = true - proxyReject(err) - } - }) - proxy.web(req, res) - }) - - return { - finished: true, - } - } - const redirects = this.minimalMode ? [] : this.customRoutes.redirects.map((redirect) => { @@ -1101,16 +1068,14 @@ export default abstract class Server { normalizeRepeatedSlashes(updatedDestination) } - res.setHeader('Location', updatedDestination) - res.statusCode = getRedirectStatus(redirectRoute as Redirect) - - // Since IE11 doesn't support the 308 header add backwards - // compatibility using refresh header - if (res.statusCode === 308) { - res.setHeader('Refresh', `0;url=${updatedDestination}`) - } + res + .redirect( + updatedDestination, + getRedirectStatus(redirectRoute as Redirect) + ) + .body(updatedDestination) + .send() - res.end(updatedDestination) return { finished: true, } @@ -1136,7 +1101,7 @@ export default abstract class Server { // external rewrite, proxy it if (parsedDestination.protocol) { - return proxyRequest(req, res, parsedDestination) + return this.proxyRequest(req, res, parsedDestination) } addRequestMeta(req, '_nextRewroteUrl', newUrl) @@ -1258,8 +1223,8 @@ export default abstract class Server { req.method === 'HEAD' && req.headers['x-middleware-preflight'] if (preflight) { - res.writeHead(200) - res.end() + res.statusCode = 200 + res.send() return { finished: true, } @@ -1275,7 +1240,7 @@ export default abstract class Server { res.setHeader('Refresh', `0;url=${location}`) } - res.end(location) + res.body(location).send() return { finished: true, } @@ -1295,7 +1260,7 @@ export default abstract class Server { ? `${parsedDestination.hostname}:${parsedDestination.port}` : parsedDestination.hostname) !== req.headers.host ) { - return proxyRequest(req, res, parsedDestination) + return this.proxyRequest(req, res, parsedDestination) } if (this.nextConfig.i18n) { @@ -1320,11 +1285,11 @@ export default abstract class Server { } if (result.response.headers.has('x-middleware-refresh')) { - res.writeHead(result.response.status) + res.statusCode = result.response.status for await (const chunk of result.response.body || ([] as any)) { - res.write(chunk) + this.streamResponseChunk(res, chunk) } - res.end() + res.send() return { finished: true, } @@ -1373,12 +1338,7 @@ export default abstract class Server { if (pathname === '/api' || pathname.startsWith('/api/')) { delete query._nextBubbleNoFallback - const handled = await this.handleApiRequest( - req as NextApiRequest, - res as NextApiResponse, - pathname, - query - ) + const handled = await this.handleApiRequest(req, res, pathname, query) if (handled) { return { finished: true } } @@ -1439,8 +1399,8 @@ export default abstract class Server { } protected async _beforeCatchAllRender( - _req: IncomingMessage, - _res: ServerResponse, + _req: BaseNextRequest, + _res: BaseNextResponse, _params: Params, _parsedUrl: UrlWithParsedQuery ): Promise { @@ -1457,13 +1417,13 @@ export default abstract class Server { * @param pathname path of request */ private async handleApiRequest( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, pathname: string, query: ParsedUrlQuery ): Promise { let page = pathname - let params: Params | boolean = false + let params: Params | false = false let pageFound = await this.hasPage(page) if (!pageFound && this.dynamicRoutes) { @@ -1494,31 +1454,7 @@ export default abstract class Server { throw err } - 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( - req, - res, - query, - pageModule, - this.renderOpts.previewProps, - this.minimalMode, - this.renderOpts.dev, - page - ) - return true + return this.runApi(req, res, query, params, page, builtPagePath) } protected getDynamicRoutes(): Array { @@ -1541,15 +1477,9 @@ export default abstract class Server { .filter((item): item is RoutingItem => Boolean(item)) } - private handleCompression(req: IncomingMessage, res: ServerResponse): void { - if (this.compression) { - this.compression(req, res, () => {}) - } - } - protected async run( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, parsedUrl: UrlWithParsedQuery ): Promise { this.handleCompression(req, res) @@ -1573,8 +1503,8 @@ export default abstract class Server { private async pipe( fn: (ctx: RequestContext) => Promise, partialContext: { - req: IncomingMessage - res: ServerResponse + req: BaseNextRequest + res: BaseNextResponse pathname: string query: NextParsedUrlQuery } @@ -1593,15 +1523,13 @@ export default abstract class Server { } const { req, res } = ctx const { body, type, revalidateOptions } = payload - if (!isResSent(res)) { + if (!res.sent) { const { generateEtags, poweredByHeader, dev } = this.renderOpts if (dev) { // In dev, we should not cache pages for any reason. res.setHeader('Cache-Control', 'no-store, must-revalidate') } - return sendRenderResult({ - req, - res, + return this.sendRenderResult(req, res, { result: body, type, generateEtags, @@ -1614,8 +1542,8 @@ export default abstract class Server { private async getStaticHTML( fn: (ctx: RequestContext) => Promise, partialContext: { - req: IncomingMessage - res: ServerResponse + req: BaseNextRequest + res: BaseNextResponse pathname: string query: ParsedUrlQuery } @@ -1634,8 +1562,8 @@ export default abstract class Server { } public async render( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, pathname: string, query: NextParsedUrlQuery = {}, parsedUrl?: NextUrlWithParsedQuery @@ -1828,13 +1756,10 @@ export default abstract class Server { redirect.destination = normalizeRepeatedSlashes(redirect.destination) } - if (statusCode === PERMANENT_REDIRECT_STATUS) { - res.setHeader('Refresh', `0;url=${redirect.destination}`) - } - - res.statusCode = statusCode - res.setHeader('Location', redirect.destination) - res.end(redirect.destination) + res + .redirect(redirect.destination, statusCode) + .body(redirect.destination) + .send() } // remove /_next/data prefix from urlPathname so it matches @@ -1940,7 +1865,7 @@ export default abstract class Server { : resolvedUrl, } - const renderResult = await renderToHTML( + const renderResult = await this.renderHTML( req, res, pathname, @@ -1975,7 +1900,7 @@ export default abstract class Server { async (hasResolved) => { const isProduction = !this.renderOpts.dev const isDynamicPathname = isDynamicRoute(pathname) - const didRespond = hasResolved || isResSent(res) + const didRespond = hasResolved || res.sent let { staticPaths, fallbackMode } = hasStaticPaths ? await this.getStaticPaths(pathname) @@ -2107,7 +2032,7 @@ export default abstract class Server { } if (isDataReq) { res.statusCode = 404 - res.end('{"notFound":true}') + res.body('{"notFound":true}').send() return null } else { await this.render404( @@ -2235,8 +2160,8 @@ export default abstract class Server { } public async renderToHTML( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, pathname: string, query: ParsedUrlQuery = {} ): Promise { @@ -2250,8 +2175,8 @@ export default abstract class Server { public async renderError( err: Error | null, - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, pathname: string, query: NextParsedUrlQuery = {}, setHeaders = true @@ -2373,8 +2298,8 @@ export default abstract class Server { public async renderErrorToHTML( err: Error | null, - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, pathname: string, query: ParsedUrlQuery = {} ): Promise { @@ -2402,8 +2327,8 @@ export default abstract class Server { } public async render404( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, parsedUrl?: NextUrlWithParsedQuery, setHeaders = true ): Promise { @@ -2423,8 +2348,8 @@ export default abstract class Server { } public async serveStatic( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, path: string, parsedUrl?: UrlWithParsedQuery ): Promise { @@ -2439,7 +2364,7 @@ export default abstract class Server { } try { - await serveStatic(req, res, path) + await this.sendStatic(req, res, path) } catch (error) { if (!isError(error)) throw error const err = error as Error & { code?: string; statusCode?: number } @@ -2499,8 +2424,8 @@ export default abstract class Server { } } -function prepareServerlessUrl( - req: IncomingMessage, +export function prepareServerlessUrl( + req: BaseNextRequest, query: ParsedUrlQuery ): void { const curUrl = parseUrl(req.url!, true) @@ -2514,6 +2439,24 @@ function prepareServerlessUrl( }) } +// since initial query values are decoded by querystring.parse +// we need to re-encode them here but still allow passing through +// values from rewrites/redirects +export const stringifyQuery = (req: BaseNextRequest, query: ParsedUrlQuery) => { + const initialQueryValues = Object.values( + getRequestMeta(req, '__NEXT_INIT_QUERY') || {} + ) + + return stringifyQs(query, undefined, undefined, { + encodeURIComponent(value) { + if (initialQueryValues.some((val) => val === value)) { + return encodeURIComponent(value) + } + return value + }, + }) +} + class NoFallbackError extends Error {} // Internal wrapper around build errors at development diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 2ab5117a021e1..9978fb28c2e4c 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -2,7 +2,6 @@ import type { __ApiPreviewProps } from '../api-utils' import type { CustomRoutes } from '../../lib/load-custom-routes' import type { FetchEventResult } from '../web/types' import type { FindComponentsResult } from '../next-server' -import type { IncomingMessage, ServerResponse } from 'http' import type { LoadComponentsReturnType } from '../load-components' import type { Options as ServerOptions } from '../next-server' import type { Params } from '../router' @@ -59,6 +58,12 @@ import * as Log from '../../build/output/log' import isError, { getProperError } from '../../lib/is-error' import { getMiddlewareRegex } from '../../shared/lib/router/utils/get-middleware-regex' import { isCustomErrorPage, isReservedPage } from '../../build/utils' +import { + BaseNextRequest, + BaseNextResponse, + NodeNextResponse, + NodeNextRequest, +} from '../base-http' // Load ReactDevOverlay only when needed let ReactDevOverlayImpl: React.FunctionComponent @@ -443,8 +448,8 @@ export default class DevServer extends Server { } protected async _beforeCatchAllRender( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, params: Params, parsedUrl: UrlWithParsedQuery ): Promise { @@ -477,10 +482,10 @@ export default class DevServer extends Server { return false } - private setupWebSocketHandler(server?: HTTPServer, _req?: IncomingMessage) { + private setupWebSocketHandler(server?: HTTPServer, _req?: NodeNextRequest) { if (!this.addedUpgradeListener) { this.addedUpgradeListener = true - server = server || (_req?.socket as any)?.server + server = server || (_req?.originalRequest.socket as any)?.server if (!server) { // this is very unlikely to happen but show an error in case @@ -519,8 +524,8 @@ export default class DevServer extends Server { } async runMiddleware(params: { - request: IncomingMessage - response: ServerResponse + request: BaseNextRequest + response: BaseNextResponse parsedUrl: ParsedNextUrl parsed: UrlWithParsedQuery }): Promise { @@ -547,8 +552,8 @@ export default class DevServer extends Server { } async run( - req: IncomingMessage, - res: ServerResponse, + req: NodeNextRequest, + res: NodeNextResponse, parsedUrl: UrlWithParsedQuery ): Promise { await this.devReady @@ -573,8 +578,8 @@ export default class DevServer extends Server { } const { finished = false } = await this.hotReloader!.run( - req, - res, + req.originalRequest, + res.originalResponse, parsedUrl ) @@ -599,7 +604,7 @@ export default class DevServer extends Server { }) } catch (internalErr) { console.error(internalErr) - res.end('Internal Server Error') + res.body('Internal Server Error').send() } } } @@ -752,11 +757,13 @@ export default class DevServer extends Server { fn: async (_req, res) => { res.statusCode = 200 res.setHeader('Content-Type', 'application/json; charset=utf-8') - res.end( - JSON.stringify({ - pages: this.sortedRoutes, - }) - ) + res + .body( + JSON.stringify({ + pages: this.sortedRoutes, + }) + ) + .send() return { finished: true, } @@ -772,14 +779,16 @@ export default class DevServer extends Server { fn: async (_req, res) => { res.statusCode = 200 res.setHeader('Content-Type', 'application/json; charset=utf-8') - res.end( - JSON.stringify( - this.middleware?.map((middleware) => [ - middleware.page, - !!middleware.ssr, - ]) || [] + res + .body( + JSON.stringify( + this.middleware?.map((middleware) => [ + middleware.page, + !!middleware.ssr, + ]) || [] + ) ) - ) + .send() return { finished: true, } @@ -927,13 +936,13 @@ export default class DevServer extends Server { return await loadDefaultErrorComponents(this.distDir) } - protected setImmutableAssetCacheControl(res: ServerResponse): void { + protected setImmutableAssetCacheControl(res: BaseNextResponse): void { res.setHeader('Cache-Control', 'no-store, must-revalidate') } private servePublic( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, pathParts: string[] ): Promise { const p = pathJoin(this.publicDir, ...pathParts) diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index a40ce81048577..de5a631573130 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -14,10 +14,10 @@ import { NextConfig } from './config-shared' import { fileExists } from '../lib/file-exists' import { ImageConfig, imageConfigDefault } from './image-config' import { processBuffer, decodeBuffer, Operation } from './lib/squoosh/main' -import type Server from './base-server' import { sendEtagResponse } from './send-payload' import { getContentType, getExtension } from './serve-static' import chalk from 'next/dist/compiled/chalk' +import { NextUrlWithParsedQuery } from './request-meta' const AVIF = 'image/avif' const WEBP = 'image/webp' @@ -47,12 +47,17 @@ try { let showSharpMissingWarning = process.env.NODE_ENV === 'production' export async function imageOptimizer( - server: Server, req: IncomingMessage, res: ServerResponse, parsedUrl: UrlWithParsedQuery, nextConfig: NextConfig, distDir: string, + render404: () => Promise, + handleRequest: ( + newReq: IncomingMessage, + newRes: ServerResponse, + newParsedUrl?: NextUrlWithParsedQuery + ) => Promise, isDev = false ) { const imageData: ImageConfig = nextConfig.images || imageConfigDefault @@ -66,7 +71,7 @@ export async function imageOptimizer( } = imageData if (loader !== 'default') { - await server.render404(req, res, parsedUrl) + await render404() return { finished: true } } @@ -282,11 +287,7 @@ export async function imageOptimizer( mockReq.url = href mockReq.connection = req.connection - await server.getRequestHandler()( - mockReq, - mockRes, - nodeUrl.parse(href, true) - ) + await handleRequest(mockReq, mockRes, nodeUrl.parse(href, true)) await isStreamFinished res.statusCode = mockRes.statusCode diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 1ee7e77538297..d9f4669e085fb 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -1,24 +1,66 @@ -import type { Route, Params } from './router' +import type { Params, Route } from './router' import type { CacheFs } from '../shared/lib/utils' -import type { NextParsedUrlQuery } from './request-meta' -import type { FontManifest } from './font-utils' +import type { NextParsedUrlQuery, NextUrlWithParsedQuery } from './request-meta' +import type RenderResult from './render-result' import fs from 'fs' import { join, relative } from 'path' +import { IncomingMessage, ServerResponse } from 'http' + +import { PAGES_MANIFEST, BUILD_ID_FILE } from '../shared/lib/constants' import { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' import { recursiveReadDirSync } from './lib/recursive-readdir-sync' +import { format as formatUrl, UrlWithParsedQuery } from 'url' +import compression from 'next/dist/compiled/compression' +import Proxy from 'next/dist/compiled/http-proxy' import { route } from './router' -import BaseServer, { FindComponentsResult } from './base-server' -import { getMiddlewareInfo } from './require' + +import { + BaseNextRequest, + BaseNextResponse, + NodeNextRequest, + NodeNextResponse, +} from './base-http' +import { PayloadOptions, sendRenderResult } from './send-payload' +import { serveStatic } from './serve-static' +import { ParsedUrlQuery } from 'querystring' +import { apiResolver } from './api-utils' +import { RenderOpts, renderToHTML } from './render' +import { ParsedUrl } from '../shared/lib/router/utils/parse-url' + +import BaseServer, { + FindComponentsResult, + prepareServerlessUrl, + stringifyQuery, +} from './base-server' +import { getMiddlewareInfo, getPagePath, requireFontManifest } from './require' +import { normalizePagePath } from './normalize-page-path' import { loadComponents } from './load-components' import isError from '../lib/is-error' -import { normalizePagePath } from './normalize-page-path' -import { getPagePath, requireFontManifest } from './require' -import { BUILD_ID_FILE, PAGES_MANIFEST } from '../shared/lib/constants' +import { FontManifest } from './font-utils' export * from './base-server' +type ExpressMiddleware = ( + req: IncomingMessage, + res: ServerResponse, + next: (err?: Error) => void +) => void + +export interface NodeRequestHandler { + ( + req: IncomingMessage | BaseNextRequest, + res: ServerResponse | BaseNextResponse, + parsedUrl?: NextUrlWithParsedQuery | undefined + ): Promise +} + export default class NextNodeServer extends BaseServer { + private compression = + this.nextConfig.compress && this.nextConfig.target === 'server' + ? (compression() as ExpressMiddleware) + : undefined + protected getHasStaticDir(): boolean { return fs.existsSync(join(this.dir, 'static')) } @@ -44,7 +86,6 @@ export default class NextNodeServer extends BaseServer { } protected generateImageRoutes(): Route[] { - const server = this return [ { match: route('/_next/image'), @@ -53,22 +94,16 @@ export default class NextNodeServer extends BaseServer { fn: (req, res, _params, parsedUrl) => { if (this.minimalMode) { res.statusCode = 400 - res.end('Bad Request') + res.body('Bad Request').send() return { finished: true, } } - const { imageOptimizer } = - require('./image-optimizer') as typeof import('./image-optimizer') - - return imageOptimizer( - server, - req, - res, - parsedUrl, - server.nextConfig, - server.distDir, - this.renderOpts.dev + + return this.imageOptimizer( + req as NodeNextRequest, + res as NodeNextResponse, + parsedUrl ) }, }, @@ -174,6 +209,164 @@ export default class NextNodeServer extends BaseServer { ])) } + protected sendRenderResult( + req: NodeNextRequest, + res: NodeNextResponse, + options: { + result: RenderResult + type: 'html' | 'json' + generateEtags: boolean + poweredByHeader: boolean + options?: PayloadOptions | undefined + } + ): Promise { + return sendRenderResult({ + req: req.originalRequest, + res: res.originalResponse, + ...options, + }) + } + + protected sendStatic( + req: NodeNextRequest, + res: NodeNextResponse, + path: string + ): Promise { + return serveStatic(req.originalRequest, res.originalResponse, path) + } + + protected handleCompression( + req: NodeNextRequest, + res: NodeNextResponse + ): void { + if (this.compression) { + this.compression(req.originalRequest, res.originalResponse, () => {}) + } + } + + protected async proxyRequest( + req: NodeNextRequest, + res: NodeNextResponse, + parsedUrl: ParsedUrl + ) { + const { query } = parsedUrl + delete (parsedUrl as any).query + parsedUrl.search = stringifyQuery(req, query) + + const target = formatUrl(parsedUrl) + const proxy = new Proxy({ + target, + changeOrigin: true, + ignorePath: true, + xfwd: true, + proxyTimeout: 30_000, // limit proxying to 30 seconds + }) + + await new Promise((proxyResolve, proxyReject) => { + let finished = false + + proxy.on('proxyReq', (proxyReq) => { + proxyReq.on('close', () => { + if (!finished) { + finished = true + proxyResolve(true) + } + }) + }) + proxy.on('error', (err) => { + if (!finished) { + finished = true + proxyReject(err) + } + }) + proxy.web(req.originalRequest, res.originalResponse) + }) + + return { + finished: true, + } + } + + protected async runApi( + req: NodeNextRequest, + res: NodeNextResponse, + query: ParsedUrlQuery, + params: Params | false, + page: string, + builtPagePath: string + ): Promise { + 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( + req.originalRequest, + res.originalResponse, + query, + pageModule, + this.renderOpts.previewProps, + this.minimalMode, + this.renderOpts.dev, + page + ) + return true + } + + protected async renderHTML( + req: NodeNextRequest, + res: NodeNextResponse, + pathname: string, + query: NextParsedUrlQuery, + renderOpts: RenderOpts + ): Promise { + return renderToHTML( + req.originalRequest, + res.originalResponse, + pathname, + query, + renderOpts + ) + } + + protected streamResponseChunk(res: NodeNextResponse, chunk: any) { + res.originalResponse.write(chunk) + } + + protected async imageOptimizer( + req: NodeNextRequest, + res: NodeNextResponse, + parsedUrl: UrlWithParsedQuery + ): Promise<{ finished: boolean }> { + const { imageOptimizer } = + require('./image-optimizer') as typeof import('./image-optimizer') + + return imageOptimizer( + req.originalRequest, + res.originalResponse, + parsedUrl, + this.nextConfig, + this.distDir, + () => this.render404(req, res, parsedUrl), + (newReq, newRes, newParsedUrl) => + this.getRequestHandler()( + new NodeNextRequest(newReq), + new NodeNextResponse(newRes), + newParsedUrl + ), + this.renderOpts.dev + ) + } + protected getPagePath(pathname: string, locales?: string[]): string { return getPagePath( pathname, @@ -257,6 +450,117 @@ export default class NextNodeServer extends BaseServer { } } + 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, + parsedUrl?: NextUrlWithParsedQuery + ): Promise { + return super.render( + this.normalizeReq(req), + this.normalizeRes(res), + pathname, + query, + parsedUrl + ) + } + + public async renderToHTML( + req: BaseNextRequest | IncomingMessage, + res: BaseNextResponse | ServerResponse, + pathname: string, + query?: ParsedUrlQuery + ): Promise { + 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 { + 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 { + 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 { + 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 { + return super.serveStatic( + this.normalizeReq(req), + this.normalizeRes(res), + path, + parsedUrl + ) + } + protected getMiddlewareInfo(params: { dev?: boolean distDir: string diff --git a/packages/next/server/next.ts b/packages/next/server/next.ts index bae67bbcfef25..31f6f75024385 100644 --- a/packages/next/server/next.ts +++ b/packages/next/server/next.ts @@ -1,6 +1,5 @@ -import type { IncomingMessage, ServerResponse } from 'http' import type { Options as DevServerOptions } from './dev/next-dev-server' -import type { RequestHandler } from './next-server' +import type { NodeRequestHandler } from './next-server' import type { UrlWithParsedQuery } from 'url' import './node-polyfill-fetch' @@ -11,6 +10,8 @@ import { resolve } from 'path' import { NON_STANDARD_NODE_ENV } from '../lib/constants' import { PHASE_DEVELOPMENT_SERVER } from '../shared/lib/constants' import { PHASE_PRODUCTION_SERVER } from '../shared/lib/constants' +import { IncomingMessage, ServerResponse } from 'http' +import { NextUrlWithParsedQuery } from './request-meta' let ServerImpl: typeof Server @@ -22,10 +23,18 @@ const getServerImpl = async () => { export type NextServerOptions = Partial +export interface RequestHandler { + ( + req: IncomingMessage, + res: ServerResponse, + parsedUrl?: NextUrlWithParsedQuery | undefined + ): Promise +} + export class NextServer { private serverPromise?: Promise private server?: Server - private reqHandlerPromise?: Promise + private reqHandlerPromise?: Promise private preparedAssetPrefix?: string public options: NextServerOptions @@ -183,4 +192,3 @@ exports = module.exports // Support `import next from 'next'` export default createServer -export type { RequestHandler } diff --git a/packages/next/server/request-meta.ts b/packages/next/server/request-meta.ts index 79866ad8913b7..0c74571af4929 100644 --- a/packages/next/server/request-meta.ts +++ b/packages/next/server/request-meta.ts @@ -1,15 +1,16 @@ /* eslint-disable no-redeclare */ +import { IncomingMessage } from 'http' import type { ParsedUrlQuery } from 'querystring' -import type { IncomingMessage } from 'http' import type { UrlWithParsedQuery } from 'url' +import { BaseNextRequest } from './base-http' -const NEXT_REQUEST_META = Symbol('NextRequestMeta') +export const NEXT_REQUEST_META = Symbol('NextRequestMeta') -interface NextIncomingMessage extends IncomingMessage { +export type NextIncomingMessage = (BaseNextRequest | IncomingMessage) & { [NEXT_REQUEST_META]?: RequestMeta } -interface RequestMeta { +export interface RequestMeta { __NEXT_INIT_QUERY?: ParsedUrlQuery __NEXT_INIT_URL?: string __nextHadTrailingSlash?: boolean diff --git a/packages/next/server/router.ts b/packages/next/server/router.ts index c4faf4767a4ed..01d2ff94f461b 100644 --- a/packages/next/server/router.ts +++ b/packages/next/server/router.ts @@ -1,4 +1,3 @@ -import type { IncomingMessage, ServerResponse } from 'http' import type { ParsedUrlQuery } from 'querystring' import type { NextUrlWithParsedQuery } from './request-meta' @@ -8,6 +7,7 @@ import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' import { RouteHas } from '../lib/load-custom-routes' import { matchHas } from '../shared/lib/router/utils/prepare-destination' import { getRequestMeta } from './request-meta' +import { BaseNextRequest, BaseNextResponse } from './base-http' export const route = pathMatch() @@ -31,8 +31,8 @@ export type Route = { requireBasePath?: false internal?: true fn: ( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, params: Params, parsedUrl: NextUrlWithParsedQuery ) => Promise | RouteResult @@ -134,8 +134,8 @@ export default class Router { } async execute( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, parsedUrl: NextUrlWithParsedQuery ): Promise { // memoize page check calls so we don't duplicate checks for pages diff --git a/packages/next/server/send-payload.ts b/packages/next/server/send-payload.ts index f27d7df288c33..138e154080ad3 100644 --- a/packages/next/server/send-payload.ts +++ b/packages/next/server/send-payload.ts @@ -3,6 +3,7 @@ import { isResSent } from '../shared/lib/utils' import generateETag from 'next/dist/compiled/etag' import fresh from 'next/dist/compiled/fresh' import RenderResult from './render-result' +import { BaseNextResponse } from './base-http' export type PayloadOptions = | { private: true } @@ -10,7 +11,7 @@ export type PayloadOptions = | { private: boolean; stateful: false; revalidate: number | false } export function setRevalidateHeaders( - res: ServerResponse, + res: ServerResponse | BaseNextResponse, options: PayloadOptions ) { if (options.private || options.stateful) { diff --git a/packages/next/shared/lib/router/utils/prepare-destination.ts b/packages/next/shared/lib/router/utils/prepare-destination.ts index 991d125f7307b..0596c2858216d 100644 --- a/packages/next/shared/lib/router/utils/prepare-destination.ts +++ b/packages/next/shared/lib/router/utils/prepare-destination.ts @@ -6,9 +6,10 @@ import type { RouteHas } from '../../../../lib/load-custom-routes' import { compile, pathToRegexp } from 'next/dist/compiled/path-to-regexp' import { escapeStringRegexp } from '../../escape-regexp' import { parseUrl } from './parse-url' +import { BaseNextRequest } from '../../../../server/base-http' export function matchHas( - req: IncomingMessage, + req: BaseNextRequest | IncomingMessage, has: RouteHas[], query: Params ): false | Params {