Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Optimize the web server size #34242

Merged
merged 5 commits into from Feb 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -7,7 +7,10 @@ import { NextRequest } from '../../../../server/web/spec-extension/request'
import { toNodeHeaders } from '../../../../server/web/utils'

import WebServer from '../../../../server/web-server'
import { WebNextRequest, WebNextResponse } from '../../../../server/base-http'
import {
WebNextRequest,
WebNextResponse,
} from '../../../../server/base-http/web'

const createHeaders = (args?: any) => ({
...args,
Expand Down
@@ -1,9 +1,12 @@
import { parse as parseUrl } from 'url'
import { IncomingMessage, ServerResponse } from 'http'
import { apiResolver } from '../../../../server/api-utils'
import { apiResolver } from '../../../../server/api-utils/node'
import { getUtils, vercelHeader, ServerlessHandlerCtx } from './utils'
import { DecodeError } from '../../../../shared/lib/utils'
import { NodeNextResponse, NodeNextRequest } from '../../../../server/base-http'
import {
NodeNextResponse,
NodeNextRequest,
} from '../../../../server/base-http/node'

export function getApiHandler(ctx: ServerlessHandlerCtx) {
const { pageModule, encodedPreviewProps, pageIsDynamic } = ctx
Expand Down
Expand Up @@ -5,7 +5,7 @@ import { sendRenderResult } from '../../../../server/send-payload'
import { getUtils, vercelHeader, ServerlessHandlerCtx } from './utils'

import { renderToHTML } from '../../../../server/render'
import { tryGetPreviewData } from '../../../../server/api-utils'
import { tryGetPreviewData } from '../../../../server/api-utils/node'
import { denormalizePagePath } from '../../../../server/denormalize-page-path'
import { setLazyProp, getCookieParser } from '../../../../server/api-utils'
import { getRedirectStatus } from '../../../../lib/load-custom-routes'
Expand Down
Expand Up @@ -7,6 +7,7 @@ import type {
GetStaticPaths,
GetStaticProps,
} from '../../../../types'
import type { BaseNextRequest } from '../../../../server/base-http'

import { format as formatUrl, UrlWithParsedQuery, parse as parseUrl } from 'url'
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
Expand All @@ -26,7 +27,6 @@ import { denormalizePagePath } from '../../../../server/denormalize-page-path'
import cookie from 'next/dist/compiled/cookie'
import { TEMPORARY_REDIRECT_STATUS } from '../../../../shared/lib/constants'
import { addRequestMeta } from '../../../../server/request-meta'
import { BaseNextRequest } from '../../../../server/base-http'

const getCustomRouteMatcher = pathMatch(true)

Expand Down
191 changes: 191 additions & 0 deletions packages/next/server/api-utils/index.ts
@@ -0,0 +1,191 @@
import type { IncomingMessage } from 'http'
import type { BaseNextRequest } from '../base-http'

import { NextApiRequest, NextApiResponse } from '../../shared/lib/utils'

export type NextApiRequestCookies = { [key: string]: string }
export type NextApiRequestQuery = { [key: string]: string | string[] }

export type __ApiPreviewProps = {
previewModeId: string
previewModeEncryptionKey: string
previewModeSigningKey: string
}

/**
* Parse cookies from the `headers` of request
* @param req request object
*/
export function getCookieParser(headers: {
[key: string]: undefined | string | string[]
}): () => NextApiRequestCookies {
return function parseCookie(): NextApiRequestCookies {
const header: undefined | string | string[] = headers.cookie

if (!header) {
return {}
}

const { parse: parseCookieFn } = require('next/dist/compiled/cookie')
return parseCookieFn(Array.isArray(header) ? header.join(';') : header)
}
}

/**
*
* @param res response object
* @param statusCode `HTTP` status code of response
*/
export function sendStatusCode(
res: NextApiResponse,
statusCode: number
): NextApiResponse<any> {
res.statusCode = statusCode
return res
}

/**
*
* @param res response object
* @param [statusOrUrl] `HTTP` status code of redirect
* @param url URL of redirect
*/
export function redirect(
res: NextApiResponse,
statusOrUrl: string | number,
url?: string
): NextApiResponse<any> {
if (typeof statusOrUrl === 'string') {
url = statusOrUrl
statusOrUrl = 307
}
if (typeof statusOrUrl !== 'number' || typeof url !== 'string') {
throw new Error(
`Invalid redirect arguments. Please use a single argument URL, e.g. res.redirect('/destination') or use a status code and URL, e.g. res.redirect(307, '/destination').`
)
}
res.writeHead(statusOrUrl, { Location: url })
res.write(url)
res.end()
return res
}

export const PRERENDER_REVALIDATE_HEADER = 'x-prerender-revalidate'

export function checkIsManualRevalidate(
req: IncomingMessage | BaseNextRequest,
previewProps: __ApiPreviewProps
): boolean {
return req.headers[PRERENDER_REVALIDATE_HEADER] === previewProps.previewModeId
}

export const COOKIE_NAME_PRERENDER_BYPASS = `__prerender_bypass`
export const COOKIE_NAME_PRERENDER_DATA = `__next_preview_data`

export const SYMBOL_PREVIEW_DATA = Symbol(COOKIE_NAME_PRERENDER_DATA)
export const SYMBOL_CLEARED_COOKIES = Symbol(COOKIE_NAME_PRERENDER_BYPASS)

export function clearPreviewData<T>(
res: NextApiResponse<T>
): NextApiResponse<T> {
if (SYMBOL_CLEARED_COOKIES in res) {
return res
}

const { serialize } =
require('next/dist/compiled/cookie') as typeof import('cookie')
const previous = res.getHeader('Set-Cookie')
res.setHeader(`Set-Cookie`, [
...(typeof previous === 'string'
? [previous]
: Array.isArray(previous)
? previous
: []),
serialize(COOKIE_NAME_PRERENDER_BYPASS, '', {
// To delete a cookie, set `expires` to a date in the past:
// https://tools.ietf.org/html/rfc6265#section-4.1.1
// `Max-Age: 0` is not valid, thus ignored, and the cookie is persisted.
expires: new Date(0),
httpOnly: true,
sameSite: process.env.NODE_ENV !== 'development' ? 'none' : 'lax',
secure: process.env.NODE_ENV !== 'development',
path: '/',
}),
serialize(COOKIE_NAME_PRERENDER_DATA, '', {
// To delete a cookie, set `expires` to a date in the past:
// https://tools.ietf.org/html/rfc6265#section-4.1.1
// `Max-Age: 0` is not valid, thus ignored, and the cookie is persisted.
expires: new Date(0),
httpOnly: true,
sameSite: process.env.NODE_ENV !== 'development' ? 'none' : 'lax',
secure: process.env.NODE_ENV !== 'development',
path: '/',
}),
])

Object.defineProperty(res, SYMBOL_CLEARED_COOKIES, {
value: true,
enumerable: false,
})
return res
}

/**
* Custom error class
*/
export class ApiError extends Error {
readonly statusCode: number

constructor(statusCode: number, message: string) {
super(message)
this.statusCode = statusCode
}
}

/**
* Sends error in `response`
* @param res response object
* @param statusCode of response
* @param message of response
*/
export function sendError(
res: NextApiResponse,
statusCode: number,
message: string
): void {
res.statusCode = statusCode
res.statusMessage = message
res.end(message)
}

interface LazyProps {
req: NextApiRequest
}

/**
* Execute getter function only if its needed
* @param LazyProps `req` and `params` for lazyProp
* @param prop name of property
* @param getter function to get data
*/
export function setLazyProp<T>(
{ req }: LazyProps,
prop: string,
getter: () => T
): void {
const opts = { configurable: true, enumerable: true }
const optsReset = { ...opts, writable: true }

Object.defineProperty(req, prop, {
...opts,
get: () => {
const value = getter()
// we set the property on the object to avoid recalculating it
Object.defineProperty(req, prop, { ...optsReset, value })
return value
},
set: (value) => {
Object.defineProperty(req, prop, { ...optsReset, value })
},
})
}