2022-02-11 20:56:25 +01:00
import type { IncomingMessage } from 'http'
import type { BaseNextRequest } from '../base-http'
import { NextApiRequest , NextApiResponse } from '../../shared/lib/utils'
fix NextApiRequestCookies and NextApiRequestQuery types (#25532)
Hello! Thanks for making next.js so great.
## Bug
Right now, these types give false confidence. These `key`s are treated as though [a value is defined for _every_ string](https://dev.to/sarioglu/avoiding-unintended-undefined-values-while-using-typescript-record-4igo). However, given an arbitrary request, a particular cookie or query param could be `undefined`.
For example, when building an `/api` endpoint, the code might look like this:
```ts
import type { NextApiRequest, NextApiResponse } from "next"
export default function handler(req: NextApiRequest, res: NextApiResponse) {
// According to the old types, `value` is a string
const value = req.cookies.value
// Type-checking passes but leads to a runtime error when no `value` cookie is provided in the request
// Uncaught TypeError: Cannot read property 'toLowerCase' of undefined
value.toLowerCause()
// ...
}
```
By using `Partial`, TypeScript now knows that these objects don't have values defined for every `key` and accessing a given `key` might resolve to `undefined`.
---
The only obvious error this caused within this repo was on line 333 of the same file. For better or worse, I ended up casting that cookie value to a `string`. There's a series of `if` statements before it that, I guess, are guaranteeing that it's truly a string. Potentially, that stretch could be refactored such that TypeScript _knows_ it's a string.
Also, I tried to follow the contributing guidelines. However, running `yarn types` kicked out a bunch of errors about overwriting files:
```
$ yarn types
yarn run v1.22.10
$ lerna run types --stream
lerna notice cli v4.0.0
lerna info Executing command in 2 packages: "yarn run types"
@next/env: $ tsc index.ts --declaration --emitDeclarationOnly --declarationDir types --esModuleInterop
next: $ tsc --declaration --emitDeclarationOnly --declarationDir dist
next: error TS5055: Cannot write file '/Users/mbrandly/code/next.js/packages/next/dist/build/index.d.ts' because it would overwrite input file.
next: error TS5055: Cannot write file '/Users/mbrandly/code/next.js/packages/next/dist/build/webpack/plugins/build-manifest-plugin.d.ts' because it would overwrite input file.
...
...
...
```
Let me know if there's anything I can improve here! Thanks again.
2022-05-23 02:48:26 +02:00
export type NextApiRequestCookies = Partial < { [ key : string ] : string } >
export type NextApiRequestQuery = Partial < { [ key : string ] : string | string [ ] } >
2022-02-11 20:56:25 +01:00
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'
2022-04-13 18:56:58 +02:00
export const PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER =
'x-prerender-revalidate-if-generated'
2022-02-11 20:56:25 +01:00
export function checkIsManualRevalidate (
req : IncomingMessage | BaseNextRequest ,
previewProps : __ApiPreviewProps
2022-04-13 18:56:58 +02:00
) : {
isManualRevalidate : boolean
revalidateOnlyGenerated : boolean
} {
return {
isManualRevalidate :
req . headers [ PRERENDER_REVALIDATE_HEADER ] === previewProps . previewModeId ,
revalidateOnlyGenerated :
! ! req . headers [ PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER ] ,
}
2022-02-11 20:56:25 +01:00
}
export const COOKIE_NAME_PRERENDER_BYPASS = ` __prerender_bypass `
export const COOKIE_NAME_PRERENDER_DATA = ` __next_preview_data `
2022-02-25 03:04:02 +01:00
export const RESPONSE_LIMIT_DEFAULT = 4 * 1024 * 1024
2022-02-11 20:56:25 +01:00
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 } )
} ,
} )
}