UNPKG

11.9 kBJavaScriptView Raw
1"use strict";exports.__esModule=true;exports.apiResolver=apiResolver;exports.parseBody=parseBody;exports.getCookieParser=getCookieParser;exports.sendStatusCode=sendStatusCode;exports.redirect=redirect;exports.sendData=sendData;exports.sendJson=sendJson;exports.tryGetPreviewData=tryGetPreviewData;exports.sendError=sendError;exports.setLazyProp=setLazyProp;exports.ApiError=exports.SYMBOL_PREVIEW_DATA=void 0;var _contentType=require("next/dist/compiled/content-type");var _rawBody=_interopRequireDefault(require("raw-body"));var _stream=require("stream");var _utils=require("../lib/utils");var _cryptoUtils=require("./crypto-utils");var _loadComponents=require("./load-components");var _sendPayload=require("./send-payload");var _etag=_interopRequireDefault(require("etag"));function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj};}async function apiResolver(req,res,query,resolverModule,apiContext,propagateError){const apiReq=req;const apiRes=res;try{var _config$api,_config$api2;if(!resolverModule){res.statusCode=404;res.end('Not Found');return;}const config=resolverModule.config||{};const bodyParser=((_config$api=config.api)==null?void 0:_config$api.bodyParser)!==false;const externalResolver=((_config$api2=config.api)==null?void 0:_config$api2.externalResolver)||false;// Parsing of cookies
2setLazyProp({req:apiReq},'cookies',getCookieParser(req));// Parsing query string
3apiReq.query=query;// Parsing preview data
4setLazyProp({req:apiReq},'previewData',()=>tryGetPreviewData(req,res,apiContext));// Checking if preview mode is enabled
5setLazyProp({req:apiReq},'preview',()=>apiReq.previewData!==false?true:undefined);// Parsing of body
6if(bodyParser&&!apiReq.body){apiReq.body=await parseBody(apiReq,config.api&&config.api.bodyParser&&config.api.bodyParser.sizeLimit?config.api.bodyParser.sizeLimit:'1mb');}apiRes.status=statusCode=>sendStatusCode(apiRes,statusCode);apiRes.send=data=>sendData(apiReq,apiRes,data);apiRes.json=data=>sendJson(apiRes,data);apiRes.redirect=(statusOrUrl,url)=>redirect(apiRes,statusOrUrl,url);apiRes.setPreviewData=(data,options={})=>setPreviewData(apiRes,data,Object.assign({},apiContext,options));apiRes.clearPreviewData=()=>clearPreviewData(apiRes);const resolver=(0,_loadComponents.interopDefault)(resolverModule);let wasPiped=false;if(process.env.NODE_ENV!=='production'){// listen for pipe event and don't show resolve warning
7res.once('pipe',()=>wasPiped=true);}// Call API route method
8await resolver(req,res);if(process.env.NODE_ENV!=='production'&&!externalResolver&&!(0,_utils.isResSent)(res)&&!wasPiped){console.warn(`API resolved without sending a response for ${req.url}, this may result in stalled requests.`);}}catch(err){if(err instanceof ApiError){sendError(apiRes,err.statusCode,err.message);}else{console.error(err);if(propagateError){throw err;}sendError(apiRes,500,'Internal Server Error');}}}/**
9 * Parse incoming message like `json` or `urlencoded`
10 * @param req request object
11 */async function parseBody(req,limit){let contentType;try{contentType=(0,_contentType.parse)(req.headers['content-type']||'text/plain');}catch(_unused){contentType=(0,_contentType.parse)('text/plain');}const{type,parameters}=contentType;const encoding=parameters.charset||'utf-8';let buffer;try{buffer=await(0,_rawBody.default)(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;}}/**
12 * Parse `JSON` and handles invalid `JSON` strings
13 * @param str `JSON` string
14 */function parseJson(str){if(str.length===0){// special-case empty json body, as it's a common client-side mistake
15return{};}try{return JSON.parse(str);}catch(e){throw new ApiError(400,'Invalid JSON');}}/**
16 * Parse cookies from `req` header
17 * @param req request object
18 */function getCookieParser(req){return function parseCookie(){const header=req.headers.cookie;if(!header){return{};}const{parse:parseCookieFn}=require('next/dist/compiled/cookie');return parseCookieFn(Array.isArray(header)?header.join(';'):header);};}/**
19 *
20 * @param res response object
21 * @param statusCode `HTTP` status code of response
22 */function sendStatusCode(res,statusCode){res.statusCode=statusCode;return res;}/**
23 *
24 * @param res response object
25 * @param [statusOrUrl] `HTTP` status code of redirect
26 * @param url URL of redirect
27 */function redirect(res,statusOrUrl,url){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('');res.end();return res;}/**
28 * Send `any` body to response
29 * @param req request object
30 * @param res response object
31 * @param body of response
32 */function sendData(req,res,body){if(body===null||body===undefined){res.end();return;}const contentType=res.getHeader('Content-Type');if(body instanceof _stream.Stream){if(!contentType){res.setHeader('Content-Type','application/octet-stream');}body.pipe(res);return;}const isJSONLike=['object','number','boolean'].includes(typeof body);const stringifiedBody=isJSONLike?JSON.stringify(body):body;const etag=(0,_etag.default)(stringifiedBody);if((0,_sendPayload.sendEtagResponse)(req,res,etag)){return;}if(Buffer.isBuffer(body)){if(!contentType){res.setHeader('Content-Type','application/octet-stream');}res.setHeader('Content-Length',body.length);res.end(body);return;}if(isJSONLike){res.setHeader('Content-Type','application/json; charset=utf-8');}res.setHeader('Content-Length',Buffer.byteLength(stringifiedBody));res.end(stringifiedBody);}/**
33 * Send `JSON` object
34 * @param res response object
35 * @param jsonBody of data
36 */function sendJson(res,jsonBody){// Set header to application/json
37res.setHeader('Content-Type','application/json; charset=utf-8');// Use send to handle request
38res.send(jsonBody);}const COOKIE_NAME_PRERENDER_BYPASS=`__prerender_bypass`;const COOKIE_NAME_PRERENDER_DATA=`__next_preview_data`;const SYMBOL_PREVIEW_DATA=Symbol(COOKIE_NAME_PRERENDER_DATA);exports.SYMBOL_PREVIEW_DATA=SYMBOL_PREVIEW_DATA;const SYMBOL_CLEARED_COOKIES=Symbol(COOKIE_NAME_PRERENDER_BYPASS);function tryGetPreviewData(req,res,options){// Read cached preview data if present
39if(SYMBOL_PREVIEW_DATA in req){return req[SYMBOL_PREVIEW_DATA];}const getCookies=getCookieParser(req);let cookies;try{cookies=getCookies();}catch(_unused2){// TODO: warn
40return false;}const hasBypass=(COOKIE_NAME_PRERENDER_BYPASS in cookies);const hasData=(COOKIE_NAME_PRERENDER_DATA in cookies);// Case: neither cookie is set.
41if(!(hasBypass||hasData)){return false;}// Case: one cookie is set, but not the other.
42if(hasBypass!==hasData){clearPreviewData(res);return false;}// Case: preview session is for an old build.
43if(cookies[COOKIE_NAME_PRERENDER_BYPASS]!==options.previewModeId){clearPreviewData(res);return false;}const tokenPreviewData=cookies[COOKIE_NAME_PRERENDER_DATA];const jsonwebtoken=require('next/dist/compiled/jsonwebtoken');let encryptedPreviewData;try{encryptedPreviewData=jsonwebtoken.verify(tokenPreviewData,options.previewModeSigningKey);}catch(_unused3){// TODO: warn
44clearPreviewData(res);return false;}const decryptedPreviewData=(0,_cryptoUtils.decryptWithSecret)(Buffer.from(options.previewModeEncryptionKey),encryptedPreviewData.data);try{// TODO: strict runtime type checking
45const data=JSON.parse(decryptedPreviewData);// Cache lookup
46Object.defineProperty(req,SYMBOL_PREVIEW_DATA,{value:data,enumerable:false});return data;}catch(_unused4){return false;}}function isNotValidData(str){return typeof str!=='string'||str.length<16;}function setPreviewData(res,data,// TODO: strict runtime type checking
47options){if(isNotValidData(options.previewModeId)){throw new Error('invariant: invalid previewModeId');}if(isNotValidData(options.previewModeEncryptionKey)){throw new Error('invariant: invalid previewModeEncryptionKey');}if(isNotValidData(options.previewModeSigningKey)){throw new Error('invariant: invalid previewModeSigningKey');}const jsonwebtoken=require('next/dist/compiled/jsonwebtoken');const payload=jsonwebtoken.sign({data:(0,_cryptoUtils.encryptWithSecret)(Buffer.from(options.previewModeEncryptionKey),JSON.stringify(data))},options.previewModeSigningKey,{algorithm:'HS256',...(options.maxAge!==undefined?{expiresIn:options.maxAge}:undefined)});// limit preview mode cookie to 2KB since we shouldn't store too much
48// data here and browsers drop cookies over 4KB
49if(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`);}const{serialize}=require('next/dist/compiled/cookie');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,sameSite:process.env.NODE_ENV!=='development'?'none':'lax',secure:process.env.NODE_ENV!=='development',path:'/',...(options.maxAge!==undefined?{maxAge:options.maxAge}:undefined)}),serialize(COOKIE_NAME_PRERENDER_DATA,payload,{httpOnly:true,sameSite:process.env.NODE_ENV!=='development'?'none':'lax',secure:process.env.NODE_ENV!=='development',path:'/',...(options.maxAge!==undefined?{maxAge:options.maxAge}:undefined)})]);return res;}function clearPreviewData(res){if(SYMBOL_CLEARED_COOKIES in res){return res;}const{serialize}=require('next/dist/compiled/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:
50// https://tools.ietf.org/html/rfc6265#section-4.1.1
51// `Max-Age: 0` is not valid, thus ignored, and the cookie is persisted.
52expires: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:
53// https://tools.ietf.org/html/rfc6265#section-4.1.1
54// `Max-Age: 0` is not valid, thus ignored, and the cookie is persisted.
55expires: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;}/**
56 * Custom error class
57 */class ApiError extends Error{constructor(statusCode,message){super(message);this.statusCode=void 0;this.statusCode=statusCode;}}/**
58 * Sends error in `response`
59 * @param res response object
60 * @param statusCode of response
61 * @param message of response
62 */exports.ApiError=ApiError;function sendError(res,statusCode,message){res.statusCode=statusCode;res.statusMessage=message;res.end(message);}/**
63 * Execute getter function only if its needed
64 * @param LazyProps `req` and `params` for lazyProp
65 * @param prop name of property
66 * @param getter function to get data
67 */function setLazyProp({req},prop,getter){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
68Object.defineProperty(req,prop,{...optsReset,value});return value;},set:value=>{Object.defineProperty(req,prop,{...optsReset,value});}});}
69//# sourceMappingURL=api-utils.js.map
\No newline at end of file