2020-05-30 21:23:24 +02:00
import { IncomingMessage , ServerResponse } from 'http'
2020-03-29 00:56:38 +01:00
import { parse } from 'next/dist/compiled/content-type'
2020-03-29 01:02:03 +01:00
import { CookieSerializeOptions } from 'next/dist/compiled/cookie'
2020-11-06 03:33:14 +01:00
import getRawBody from 'raw-body'
2020-05-30 21:23:24 +02:00
import { PageConfig } from 'next/types'
2020-02-12 02:16:42 +01:00
import { Stream } from 'stream'
import { isResSent , NextApiRequest , NextApiResponse } from '../lib/utils'
import { decryptWithSecret , encryptWithSecret } from './crypto-utils'
2019-07-07 23:45:40 +02:00
import { interopDefault } from './load-components'
2020-11-10 05:40:26 +01:00
import { sendEtagResponse } from './send-payload'
import generateETag from 'etag'
2019-06-28 11:31:32 +02:00
export type NextApiRequestCookies = { [ key : string ] : string }
export type NextApiRequestQuery = { [ key : string ] : string | string [ ] }
2019-06-05 13:22:09 +02:00
2020-02-12 02:16:42 +01:00
export type __ApiPreviewProps = {
previewModeId : string
previewModeEncryptionKey : string
previewModeSigningKey : string
}
2019-07-07 23:45:40 +02:00
export async function apiResolver (
2019-12-10 16:08:42 +01:00
req : IncomingMessage ,
res : ServerResponse ,
2020-07-29 16:19:25 +02:00
query : any ,
2019-11-16 03:00:24 +01:00
resolverModule : any ,
2020-02-12 02:16:42 +01:00
apiContext : __ApiPreviewProps ,
2020-06-02 01:12:45 +02:00
propagateError : boolean ,
2019-11-16 03:00:24 +01:00
onError ? : ( { err } : { err : any } ) = > Promise < void >
2021-01-26 10:52:00 +01:00
) : Promise < void > {
2019-12-10 16:08:42 +01:00
const apiReq = req as NextApiRequest
const apiRes = res as NextApiResponse
2019-07-07 23:45:40 +02:00
try {
if ( ! resolverModule ) {
res . statusCode = 404
res . end ( 'Not Found' )
return
}
2020-03-26 13:32:41 +01:00
const config : PageConfig = resolverModule . config || { }
const bodyParser = config . api ? . bodyParser !== false
2020-05-07 14:05:41 +02:00
const externalResolver = config . api ? . externalResolver || false
2020-03-26 13:32:41 +01:00
2019-07-07 23:45:40 +02:00
// Parsing of cookies
2019-12-10 16:08:42 +01:00
setLazyProp ( { req : apiReq } , 'cookies' , getCookieParser ( req ) )
2019-07-07 23:45:40 +02:00
// Parsing query string
2020-07-29 16:19:25 +02:00
apiReq . query = query
2020-07-07 05:41:16 +02:00
// Parsing preview data
setLazyProp ( { req : apiReq } , 'previewData' , ( ) = >
tryGetPreviewData ( req , res , apiContext )
)
// Checking if preview mode is enabled
setLazyProp ( { req : apiReq } , 'preview' , ( ) = >
apiReq . previewData !== false ? true : undefined
)
// Parsing of body
2020-12-07 14:30:38 +01:00
if ( bodyParser && ! apiReq . body ) {
2019-12-10 16:08:42 +01:00
apiReq . body = await parseBody (
apiReq ,
2019-07-25 17:43:55 +02:00
config . api && config . api . bodyParser && config . api . bodyParser . sizeLimit
? config . api . bodyParser . sizeLimit
: '1mb'
)
2019-07-07 23:45:40 +02:00
}
2020-05-18 21:24:37 +02:00
apiRes . status = ( statusCode ) = > sendStatusCode ( apiRes , statusCode )
2020-05-30 21:23:24 +02:00
apiRes . send = ( data ) = > sendData ( apiReq , apiRes , data )
2020-05-18 21:24:37 +02:00
apiRes . json = ( data ) = > sendJson ( apiRes , data )
2020-07-29 09:01:21 +02:00
apiRes . redirect = ( statusOrUrl : number | string , url? : string ) = >
redirect ( apiRes , statusOrUrl , url )
2020-02-12 02:16:42 +01:00
apiRes . setPreviewData = ( data , options = { } ) = >
setPreviewData ( apiRes , data , Object . assign ( { } , apiContext , options ) )
apiRes . clearPreviewData = ( ) = > clearPreviewData ( apiRes )
2019-07-07 23:45:40 +02:00
const resolver = interopDefault ( resolverModule )
2020-02-03 04:48:00 +01:00
let wasPiped = false
if ( process . env . NODE_ENV !== 'production' ) {
// listen for pipe event and don't show resolve warning
res . once ( 'pipe' , ( ) = > ( wasPiped = true ) )
}
// Call API route method
2020-01-03 00:47:39 +01:00
await resolver ( req , res )
2020-01-10 16:56:20 +01:00
2020-05-07 14:05:41 +02:00
if (
process . env . NODE_ENV !== 'production' &&
! externalResolver &&
! isResSent ( res ) &&
! wasPiped
) {
2020-01-10 16:56:20 +01:00
console . warn (
2020-01-27 14:05:31 +01:00
` API resolved without sending a response for ${ req . url } , this may result in stalled requests. `
2020-01-10 16:56:20 +01:00
)
}
2019-11-16 03:00:24 +01:00
} catch ( err ) {
if ( err instanceof ApiError ) {
2019-12-10 16:08:42 +01:00
sendError ( apiRes , err . statusCode , err . message )
2019-07-07 23:45:40 +02:00
} else {
2019-11-16 03:00:24 +01:00
console . error ( err )
if ( onError ) await onError ( { err } )
2020-06-02 01:12:45 +02:00
if ( propagateError ) {
throw err
}
2019-12-10 16:08:42 +01:00
sendError ( apiRes , 500 , 'Internal Server Error' )
2019-07-07 23:45:40 +02:00
}
}
}
2019-06-05 13:22:09 +02:00
/ * *
* Parse incoming message like ` json ` or ` urlencoded `
2019-06-28 11:31:32 +02:00
* @param req request object
2019-06-05 13:22:09 +02:00
* /
2020-05-27 17:43:10 +02:00
export async function parseBody (
req : NextApiRequest ,
limit : string | number
) : Promise < any > {
2019-06-05 13:22:09 +02:00
const contentType = parse ( req . headers [ 'content-type' ] || 'text/plain' )
const { type , parameters } = contentType
const encoding = parameters . charset || 'utf-8'
let buffer
try {
buffer = await getRawBody ( req , { encoding , limit } )
} catch ( e ) {
if ( e . type === 'entity.too.large' ) {
throw new ApiError ( 413 , ` Body exceeded ${ limit } limit ` )
} else {
throw new ApiError ( 400 , 'Invalid body' )
}
}
const body = buffer . toString ( )
if ( type === 'application/json' || type === 'application/ld+json' ) {
return parseJson ( body )
} else if ( type === 'application/x-www-form-urlencoded' ) {
const qs = require ( 'querystring' )
return qs . decode ( body )
} else {
return body
}
}
/ * *
* Parse ` JSON ` and handles invalid ` JSON ` strings
* @param str ` JSON ` string
* /
2020-05-27 17:43:10 +02:00
function parseJson ( str : string ) : object {
2019-12-26 20:23:06 +01:00
if ( str . length === 0 ) {
// special-case empty json body, as it's a common client-side mistake
return { }
}
2019-06-05 13:22:09 +02:00
try {
return JSON . parse ( str )
} catch ( e ) {
throw new ApiError ( 400 , 'Invalid JSON' )
}
}
2019-06-28 11:31:32 +02:00
/ * *
2019-11-02 01:45:16 +01:00
* Parse cookies from ` req ` header
2019-06-28 11:31:32 +02:00
* @param req request object
* /
2021-01-26 10:52:00 +01:00
export function getCookieParser (
req : IncomingMessage
) : ( ) = > NextApiRequestCookies {
2019-06-28 11:31:32 +02:00
return function parseCookie ( ) : NextApiRequestCookies {
const header : undefined | string | string [ ] = req . headers . cookie
if ( ! header ) {
return { }
}
2020-06-01 23:00:22 +02:00
const { parse : parseCookieFn } = require ( 'next/dist/compiled/cookie' )
return parseCookieFn ( Array . isArray ( header ) ? header . join ( ';' ) : header )
2019-06-05 13:22:09 +02:00
}
}
/ * *
*
* @param res response object
* @param statusCode ` HTTP ` status code of response
* /
2020-05-27 17:43:10 +02:00
export function sendStatusCode (
res : NextApiResponse ,
statusCode : number
) : NextApiResponse < any > {
2019-06-05 13:22:09 +02:00
res . statusCode = statusCode
return res
}
2020-07-07 07:06:16 +02:00
/ * *
*
* @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
}
2020-08-07 00:53:09 +02:00
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'). `
)
}
2021-01-04 11:14:15 +01:00
res . writeHead ( statusOrUrl , { Location : url } )
res . write ( '' )
res . end ( )
2020-07-07 07:06:16 +02:00
return res
}
2019-06-05 13:22:09 +02:00
/ * *
* Send ` any ` body to response
2020-05-30 21:23:24 +02:00
* @param req request object
2019-06-05 13:22:09 +02:00
* @param res response object
* @param body of response
* /
2020-05-30 21:23:24 +02:00
export function sendData (
req : NextApiRequest ,
res : NextApiResponse ,
body : any
) : void {
2021-01-26 16:24:48 +01:00
if ( body === null || body === undefined ) {
2019-06-05 13:22:09 +02:00
res . end ( )
return
}
const contentType = res . getHeader ( 'Content-Type' )
2020-05-30 21:23:24 +02:00
if ( body instanceof Stream ) {
2019-06-05 13:22:09 +02:00
if ( ! contentType ) {
res . setHeader ( 'Content-Type' , 'application/octet-stream' )
}
2020-05-30 21:23:24 +02:00
body . pipe ( res )
2019-06-05 13:22:09 +02:00
return
}
2020-05-30 21:23:24 +02:00
const isJSONLike = [ 'object' , 'number' , 'boolean' ] . includes ( typeof body )
const stringifiedBody = isJSONLike ? JSON . stringify ( body ) : body
2020-11-10 05:40:26 +01:00
const etag = generateETag ( stringifiedBody )
if ( sendEtagResponse ( req , res , etag ) ) {
2020-05-30 21:23:24 +02:00
return
}
if ( Buffer . isBuffer ( body ) ) {
2019-06-05 13:22:09 +02:00
if ( ! contentType ) {
res . setHeader ( 'Content-Type' , 'application/octet-stream' )
}
2020-05-30 21:23:24 +02:00
res . setHeader ( 'Content-Length' , body . length )
res . end ( body )
2019-06-05 13:22:09 +02:00
return
}
2020-05-30 21:23:24 +02:00
if ( isJSONLike ) {
2019-06-05 13:22:09 +02:00
res . setHeader ( 'Content-Type' , 'application/json; charset=utf-8' )
}
2020-05-30 21:23:24 +02:00
res . setHeader ( 'Content-Length' , Buffer . byteLength ( stringifiedBody ) )
res . end ( stringifiedBody )
2019-06-05 13:22:09 +02:00
}
/ * *
* Send ` JSON ` object
* @param res response object
* @param jsonBody of data
* /
export function sendJson ( res : NextApiResponse , jsonBody : any ) : void {
// Set header to application/json
res . setHeader ( 'Content-Type' , 'application/json; charset=utf-8' )
// Use send to handle request
res . send ( jsonBody )
}
2020-02-12 02:16:42 +01:00
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 )
2020-03-02 11:58:47 +01:00
const SYMBOL_CLEARED_COOKIES = Symbol ( COOKIE_NAME_PRERENDER_BYPASS )
2020-02-12 02:16:42 +01:00
export function tryGetPreviewData (
req : IncomingMessage ,
res : ServerResponse ,
options : __ApiPreviewProps
) : object | string | false {
// Read cached preview data if present
if ( SYMBOL_PREVIEW_DATA in req ) {
return ( req as any ) [ SYMBOL_PREVIEW_DATA ] as any
}
const getCookies = getCookieParser ( req )
let cookies : NextApiRequestCookies
try {
cookies = getCookies ( )
} catch {
// TODO: warn
return false
}
const hasBypass = COOKIE_NAME_PRERENDER_BYPASS in cookies
const hasData = COOKIE_NAME_PRERENDER_DATA in cookies
// Case: neither cookie is set.
if ( ! ( hasBypass || hasData ) ) {
return false
}
// Case: one cookie is set, but not the other.
if ( hasBypass !== hasData ) {
clearPreviewData ( res as NextApiResponse )
return false
}
// Case: preview session is for an old build.
if ( cookies [ COOKIE_NAME_PRERENDER_BYPASS ] !== options . previewModeId ) {
clearPreviewData ( res as NextApiResponse )
return false
}
const tokenPreviewData = cookies [ COOKIE_NAME_PRERENDER_DATA ]
2020-03-29 01:37:45 +01:00
const jsonwebtoken = require ( 'next/dist/compiled/jsonwebtoken' ) as typeof import ( 'jsonwebtoken' )
2020-05-02 07:20:32 +02:00
let encryptedPreviewData : {
data : string
}
2020-02-12 02:16:42 +01:00
try {
encryptedPreviewData = jsonwebtoken . verify (
tokenPreviewData ,
options . previewModeSigningKey
2020-05-02 07:20:32 +02:00
) as typeof encryptedPreviewData
2020-02-12 02:16:42 +01:00
} catch {
// TODO: warn
clearPreviewData ( res as NextApiResponse )
return false
}
const decryptedPreviewData = decryptWithSecret (
Buffer . from ( options . previewModeEncryptionKey ) ,
2020-05-02 07:20:32 +02:00
encryptedPreviewData . data
2020-02-12 02:16:42 +01:00
)
try {
// TODO: strict runtime type checking
const data = JSON . parse ( decryptedPreviewData )
// Cache lookup
Object . defineProperty ( req , SYMBOL_PREVIEW_DATA , {
value : data ,
enumerable : false ,
} )
return data
} catch {
return false
}
}
2021-01-26 10:52:00 +01:00
function isNotValidData ( str : string ) : boolean {
return typeof str !== 'string' || str . length < 16
}
2020-02-12 02:16:42 +01:00
function setPreviewData < T > (
res : NextApiResponse < T > ,
data : object | string , // TODO: strict runtime type checking
options : {
maxAge? : number
} & __ApiPreviewProps
) : NextApiResponse < T > {
2021-01-26 10:52:00 +01:00
if ( isNotValidData ( options . previewModeId ) ) {
2020-02-12 02:16:42 +01:00
throw new Error ( 'invariant: invalid previewModeId' )
}
2021-01-26 10:52:00 +01:00
if ( isNotValidData ( options . previewModeEncryptionKey ) ) {
2020-02-12 02:16:42 +01:00
throw new Error ( 'invariant: invalid previewModeEncryptionKey' )
}
2021-01-26 10:52:00 +01:00
if ( isNotValidData ( options . previewModeSigningKey ) ) {
2020-02-12 02:16:42 +01:00
throw new Error ( 'invariant: invalid previewModeSigningKey' )
}
2020-03-29 01:37:45 +01:00
const jsonwebtoken = require ( 'next/dist/compiled/jsonwebtoken' ) as typeof import ( 'jsonwebtoken' )
2020-02-12 02:16:42 +01:00
const payload = jsonwebtoken . sign (
2020-05-02 07:20:32 +02:00
{
data : encryptWithSecret (
Buffer . from ( options . previewModeEncryptionKey ) ,
JSON . stringify ( data )
) ,
} ,
2020-02-12 02:16:42 +01:00
options . previewModeSigningKey ,
{
algorithm : 'HS256' ,
. . . ( options . maxAge !== undefined
? { expiresIn : options.maxAge }
: undefined ) ,
}
)
2020-03-04 21:37:53 +01:00
// limit preview mode cookie to 2KB since we shouldn't store too much
// data here and browsers drop cookies over 4KB
if ( payload . length > 2048 ) {
throw new Error (
` Preview data is limited to 2KB currently, reduce how much data you are storing as preview data to continue `
)
}
2020-03-29 01:02:03 +01:00
const {
serialize ,
} = require ( 'next/dist/compiled/cookie' ) as typeof import ( 'cookie' )
2020-02-12 02:16:42 +01:00
const previous = res . getHeader ( 'Set-Cookie' )
res . setHeader ( ` Set-Cookie ` , [
. . . ( typeof previous === 'string'
? [ previous ]
: Array . isArray ( previous )
? previous
: [ ] ) ,
serialize ( COOKIE_NAME_PRERENDER_BYPASS , options . previewModeId , {
httpOnly : true ,
2020-04-03 19:18:04 +02:00
sameSite : process.env.NODE_ENV !== 'development' ? 'none' : 'lax' ,
secure : process.env.NODE_ENV !== 'development' ,
2020-02-12 02:16:42 +01:00
path : '/' ,
. . . ( options . maxAge !== undefined
? ( { maxAge : options.maxAge } as CookieSerializeOptions )
: undefined ) ,
} ) ,
serialize ( COOKIE_NAME_PRERENDER_DATA , payload , {
httpOnly : true ,
2020-04-03 19:18:04 +02:00
sameSite : process.env.NODE_ENV !== 'development' ? 'none' : 'lax' ,
secure : process.env.NODE_ENV !== 'development' ,
2020-02-12 02:16:42 +01:00
path : '/' ,
. . . ( options . maxAge !== undefined
? ( { maxAge : options.maxAge } as CookieSerializeOptions )
: undefined ) ,
} ) ,
] )
return res
}
function clearPreviewData < T > ( res : NextApiResponse < T > ) : NextApiResponse < T > {
2020-03-02 11:58:47 +01:00
if ( SYMBOL_CLEARED_COOKIES in res ) {
return res
}
2020-03-29 01:02:03 +01:00
const {
serialize ,
} = require ( 'next/dist/compiled/cookie' ) as typeof import ( 'cookie' )
2020-02-12 02:16:42 +01:00
const previous = res . getHeader ( 'Set-Cookie' )
res . setHeader ( ` Set-Cookie ` , [
. . . ( typeof previous === 'string'
? [ previous ]
: Array . isArray ( previous )
? previous
: [ ] ) ,
serialize ( COOKIE_NAME_PRERENDER_BYPASS , '' , {
2020-02-21 22:05:43 +01:00
// 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 ) ,
2020-02-12 02:16:42 +01:00
httpOnly : true ,
2020-04-03 19:18:04 +02:00
sameSite : process.env.NODE_ENV !== 'development' ? 'none' : 'lax' ,
secure : process.env.NODE_ENV !== 'development' ,
2020-02-12 02:16:42 +01:00
path : '/' ,
} ) ,
serialize ( COOKIE_NAME_PRERENDER_DATA , '' , {
2020-02-21 22:05:43 +01:00
// 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 ) ,
2020-02-12 02:16:42 +01:00
httpOnly : true ,
2020-04-03 19:18:04 +02:00
sameSite : process.env.NODE_ENV !== 'development' ? 'none' : 'lax' ,
secure : process.env.NODE_ENV !== 'development' ,
2020-02-12 02:16:42 +01:00
path : '/' ,
} ) ,
] )
2020-03-02 11:58:47 +01:00
Object . defineProperty ( res , SYMBOL_CLEARED_COOKIES , {
value : true ,
enumerable : false ,
} )
2020-02-12 02:16:42 +01:00
return res
}
2019-06-05 13:22:09 +02:00
/ * *
* 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
2020-05-27 17:43:10 +02:00
) : void {
2019-06-05 13:22:09 +02:00
res . statusCode = statusCode
res . statusMessage = message
2019-07-17 02:35:13 +02:00
res . end ( message )
2019-06-05 13:22:09 +02:00
}
2019-06-28 11:31:32 +02:00
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 > (
2021-03-24 17:50:16 +01:00
{ req } : LazyProps ,
2019-06-28 11:31:32 +02:00
prop : string ,
getter : ( ) = > T
2020-05-27 17:43:10 +02:00
) : void {
2019-06-28 11:31:32 +02:00
const opts = { configurable : true , enumerable : true }
const optsReset = { . . . opts , writable : true }
Object . defineProperty ( req , prop , {
. . . opts ,
get : ( ) = > {
2021-03-24 17:50:16 +01:00
const value = getter ( )
2019-06-28 11:31:32 +02:00
// we set the property on the object to avoid recalculating it
Object . defineProperty ( req , prop , { . . . optsReset , value } )
return value
} ,
2020-05-18 21:24:37 +02:00
set : ( value ) = > {
2019-06-28 11:31:32 +02:00
Object . defineProperty ( req , prop , { . . . optsReset , value } )
} ,
} )
}