305214476b
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.
203 lines
5.5 KiB
TypeScript
203 lines
5.5 KiB
TypeScript
import type { IncomingMessage } from 'http'
|
|
import type { BaseNextRequest } from '../base-http'
|
|
|
|
import { NextApiRequest, NextApiResponse } from '../../shared/lib/utils'
|
|
|
|
export type NextApiRequestCookies = Partial<{ [key: string]: string }>
|
|
export type NextApiRequestQuery = Partial<{ [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 const PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER =
|
|
'x-prerender-revalidate-if-generated'
|
|
|
|
export function checkIsManualRevalidate(
|
|
req: IncomingMessage | BaseNextRequest,
|
|
previewProps: __ApiPreviewProps
|
|
): {
|
|
isManualRevalidate: boolean
|
|
revalidateOnlyGenerated: boolean
|
|
} {
|
|
return {
|
|
isManualRevalidate:
|
|
req.headers[PRERENDER_REVALIDATE_HEADER] === previewProps.previewModeId,
|
|
revalidateOnlyGenerated:
|
|
!!req.headers[PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER],
|
|
}
|
|
}
|
|
|
|
export const COOKIE_NAME_PRERENDER_BYPASS = `__prerender_bypass`
|
|
export const COOKIE_NAME_PRERENDER_DATA = `__next_preview_data`
|
|
|
|
export const RESPONSE_LIMIT_DEFAULT = 4 * 1024 * 1024
|
|
|
|
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 })
|
|
},
|
|
})
|
|
}
|