mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-06 12:57:46 +00:00
The error messages shown during `vc dev` when an Edge or Serverless functions returns a promise that ends up being rejected could be better.
Main changes:
- removed "socket hang up" error messages because they were not helpful to the user
- changed serverless function error handling to log explicitly and exit
- changed serverless function error message to include the request path
- changed edge function error message to include request path and a faked (but useful) stack trace
## Current
### In Production
**Serverless Function:** In production, for a serverless function rejected promise, you'll see this in the logs:
```
Unhandled Promise Rejection {
"errorType": "Runtime.UnhandledPromiseRejection",
"errorMessage": "Error: intentional break!",
"reason": {
"errorType": "Error",
"errorMessage": "intentional break!",
"stack": [
"Error: intentional break!",
" at handler (/var/task/api/node.js:3:9)",
" at Server.<anonymous> (/var/task/___vc/__helpers.js:813:19)",
" at Server.emit (node:events:527:28)",
" at parserOnIncoming (node:_http_server:956:12)",
" at HTTPParser.parserOnHeadersComplete (node:_http_common:128:17)"
]
},
"promise": {},
"stack": [
"Runtime.UnhandledPromiseRejection: Error: intentional break!",
" at process.<anonymous> (file:///var/runtime/index.mjs:1194:17)",
" at process.emit (node:events:539:35)",
" at process.emit (/var/task/___vc/__sourcemap_support.js:559:21)",
" at emit (node:internal/process/promises:140:20)",
" at processPromiseRejections (node:internal/process/promises:274:27)",
" at processTicksAndRejections (node:internal/process/task_queues:97:32)"
]
}
Unknown application error occurred
Runtime.Unknown
```
**Edge Function:** In production, for an edge function rejected promise, you'll see this in the logs:
```
Error: intentional break!
at (api/edge.js:10:10)
```
In both cases, in the browser, you see the "This Serverless/Edge Function has crashed." template with no error message or stack trace.
### In `vc dev`
**Serverless Function:** In `vc dev`, for a serverless function rejected promise, you'll see this in the output:
```
Unhandled rejection: Error: intentional break!
at handler (/Users/smassa/source/demo/edge-errors/api/node.js:3:9)
at Server.<anonymous> (/Users/smassa/source/vercel/vercel/packages/node-bridge/helpers.js:813:19)
at Server.emit (node:events:513:28)
at Server.emit (node:domain:489:12)
at parserOnIncoming (node:_http_server:998:12)
at HTTPParser.parserOnHeadersComplete (node:_http_common:128:17)
Error: Failed to complete request to /api/node: Error: socket hang up
```
**Edge Function:** In `vc dev`, for an edge function rejected promise, you'll see this in the output:
```
Unhandled rejection: intentional break!
Error: Failed to complete request to /api/edge: Error: socket hang up
```
## After Changes
### In `vc dev`
**Serverless Function:** In `vc dev`, for a serverless function rejected promise, you'll see this in the output:
```
Rejected Promise returned from /api/node: Error: intentional break!
at handler (/Users/smassa/source/demo/edge-errors/api/node.js:3:9)
at Server.<anonymous> (/Users/smassa/source/vercel/vercel/packages/node-bridge/helpers.js:824:19)
at Server.emit (node:events:513:28)
at Server.emit (node:domain:489:12)
at parserOnIncoming (node:_http_server:998:12)
at HTTPParser.parserOnHeadersComplete (node:_http_common:128:17)
```
**Edge Function:** In `vc dev`, for an edge function rejected promise, you'll see this in the output:
```
Rejected Promise returned from api/edge: intentional break!
at (api/edge.ts)
```
We can't show the real stack trace for Edge Functions because the bundling + VM execution mangles it. What's rendered here is a fake one, but it's still useful to the user.
If we currently showed the real stack trace for edge functions, it would look like:
```
Rejected Promise returned from api/edge: intentional break!
at edge (evalmachine.<anonymous>:35:9)
at FetchEvent.<anonymous> (evalmachine.<anonymous>:87:26)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async EdgeRuntime.dispatchFetch (evalmachine.<anonymous>:29:7)
at async Server.handler (/Users/smassa/source/vercel/vercel/node_modules/.pnpm/edge-runtime@2.0.0/node_modules/edge-runtime/src/server/create-handler.ts:46:26)
```
## Follow Up
We'll look into improving the Edge Function error stack traces later.
312 lines
8.3 KiB
TypeScript
312 lines
8.3 KiB
TypeScript
import type {
|
|
VercelRequest,
|
|
VercelResponse,
|
|
VercelRequestCookies,
|
|
VercelRequestQuery,
|
|
VercelRequestBody,
|
|
} from './types';
|
|
import { Server } from 'http';
|
|
import type { Bridge } from './bridge';
|
|
|
|
function getBodyParser(req: VercelRequest, body: Buffer) {
|
|
return function parseBody(): VercelRequestBody {
|
|
if (!req.headers['content-type']) {
|
|
return undefined;
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const { parse: parseContentType } = require('content-type');
|
|
const { type } = parseContentType(req.headers['content-type']);
|
|
|
|
if (type === 'application/json') {
|
|
try {
|
|
const str = body.toString();
|
|
return str ? JSON.parse(str) : {};
|
|
} catch (error) {
|
|
throw new ApiError(400, 'Invalid JSON');
|
|
}
|
|
}
|
|
|
|
if (type === 'application/octet-stream') {
|
|
return body;
|
|
}
|
|
|
|
if (type === 'application/x-www-form-urlencoded') {
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const { parse: parseQS } = require('querystring');
|
|
// note: querystring.parse does not produce an iterable object
|
|
// https://nodejs.org/api/querystring.html#querystring_querystring_parse_str_sep_eq_options
|
|
return parseQS(body.toString());
|
|
}
|
|
|
|
if (type === 'text/plain') {
|
|
return body.toString();
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
}
|
|
|
|
function getQueryParser({ url = '/' }: VercelRequest) {
|
|
return function parseQuery(): VercelRequestQuery {
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const { parse: parseURL } = require('url');
|
|
return parseURL(url, true).query;
|
|
};
|
|
}
|
|
|
|
function getCookieParser(req: VercelRequest) {
|
|
return function parseCookie(): VercelRequestCookies {
|
|
const header: undefined | string | string[] = req.headers.cookie;
|
|
|
|
if (!header) {
|
|
return {};
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const { parse } = require('cookie');
|
|
return parse(Array.isArray(header) ? header.join(';') : header);
|
|
};
|
|
}
|
|
|
|
function status(res: VercelResponse, statusCode: number): VercelResponse {
|
|
res.statusCode = statusCode;
|
|
return res;
|
|
}
|
|
|
|
function redirect(
|
|
res: VercelResponse,
|
|
statusOrUrl: string | number,
|
|
url?: string
|
|
): VercelResponse {
|
|
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 }).end();
|
|
return res;
|
|
}
|
|
|
|
function setCharset(type: string, charset: string) {
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const { parse, format } = require('content-type');
|
|
const parsed = parse(type);
|
|
parsed.parameters.charset = charset;
|
|
return format(parsed);
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function createETag(body: any, encoding: 'utf8' | undefined) {
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const etag = require('etag');
|
|
const buf = !Buffer.isBuffer(body) ? Buffer.from(body, encoding) : body;
|
|
return etag(buf, { weak: true });
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function send(
|
|
req: VercelRequest,
|
|
res: VercelResponse,
|
|
body: any
|
|
): VercelResponse {
|
|
let chunk: unknown = body;
|
|
let encoding: 'utf8' | undefined;
|
|
|
|
switch (typeof chunk) {
|
|
// string defaulting to html
|
|
case 'string':
|
|
if (!res.getHeader('content-type')) {
|
|
res.setHeader('content-type', 'text/html');
|
|
}
|
|
break;
|
|
case 'boolean':
|
|
case 'number':
|
|
case 'object':
|
|
if (chunk === null) {
|
|
chunk = '';
|
|
} else if (Buffer.isBuffer(chunk)) {
|
|
if (!res.getHeader('content-type')) {
|
|
res.setHeader('content-type', 'application/octet-stream');
|
|
}
|
|
} else {
|
|
return json(req, res, chunk);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// write strings in utf-8
|
|
if (typeof chunk === 'string') {
|
|
encoding = 'utf8';
|
|
|
|
// reflect this in content-type
|
|
const type = res.getHeader('content-type');
|
|
if (typeof type === 'string') {
|
|
res.setHeader('content-type', setCharset(type, 'utf-8'));
|
|
}
|
|
}
|
|
|
|
// populate Content-Length
|
|
let len: number | undefined;
|
|
if (chunk !== undefined) {
|
|
if (Buffer.isBuffer(chunk)) {
|
|
// get length of Buffer
|
|
len = chunk.length;
|
|
} else if (typeof chunk === 'string') {
|
|
if (chunk.length < 1000) {
|
|
// just calculate length small chunk
|
|
len = Buffer.byteLength(chunk, encoding);
|
|
} else {
|
|
// convert chunk to Buffer and calculate
|
|
const buf = Buffer.from(chunk, encoding);
|
|
len = buf.length;
|
|
chunk = buf;
|
|
encoding = undefined;
|
|
}
|
|
} else {
|
|
throw new Error(
|
|
'`body` is not a valid string, object, boolean, number, Stream, or Buffer'
|
|
);
|
|
}
|
|
|
|
if (len !== undefined) {
|
|
res.setHeader('content-length', len);
|
|
}
|
|
}
|
|
|
|
// populate ETag
|
|
let etag: string | undefined;
|
|
if (
|
|
!res.getHeader('etag') &&
|
|
len !== undefined &&
|
|
(etag = createETag(chunk, encoding))
|
|
) {
|
|
res.setHeader('etag', etag);
|
|
}
|
|
|
|
// strip irrelevant headers
|
|
if (204 === res.statusCode || 304 === res.statusCode) {
|
|
res.removeHeader('Content-Type');
|
|
res.removeHeader('Content-Length');
|
|
res.removeHeader('Transfer-Encoding');
|
|
chunk = '';
|
|
}
|
|
|
|
if (req.method === 'HEAD') {
|
|
// skip body for HEAD
|
|
res.end();
|
|
} else if (encoding) {
|
|
// respond with encoding
|
|
res.end(chunk, encoding);
|
|
} else {
|
|
// respond without encoding
|
|
res.end(chunk);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function json(
|
|
req: VercelRequest,
|
|
res: VercelResponse,
|
|
jsonBody: any
|
|
): VercelResponse {
|
|
const body = JSON.stringify(jsonBody);
|
|
|
|
// content-type
|
|
if (!res.getHeader('content-type')) {
|
|
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
}
|
|
|
|
return send(req, res, body);
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
readonly statusCode: number;
|
|
|
|
constructor(statusCode: number, message: string) {
|
|
super(message);
|
|
this.statusCode = statusCode;
|
|
}
|
|
}
|
|
|
|
export function sendError(
|
|
res: VercelResponse,
|
|
statusCode: number,
|
|
message: string
|
|
) {
|
|
res.statusCode = statusCode;
|
|
res.statusMessage = message;
|
|
res.end();
|
|
}
|
|
|
|
function setLazyProp<T>(req: VercelRequest, prop: string, getter: () => T) {
|
|
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 });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function createServerWithHelpers(
|
|
handler: (req: VercelRequest, res: VercelResponse) => void | Promise<void>,
|
|
bridge: Bridge
|
|
) {
|
|
const server = new Server(async (_req, _res) => {
|
|
const req = _req as VercelRequest;
|
|
const res = _res as VercelResponse;
|
|
|
|
try {
|
|
const reqId = req.headers['x-now-bridge-request-id'];
|
|
|
|
// don't expose this header to the client
|
|
delete req.headers['x-now-bridge-request-id'];
|
|
|
|
if (typeof reqId !== 'string') {
|
|
throw new ApiError(500, 'Internal Server Error');
|
|
}
|
|
|
|
const event = bridge.consumeEvent(reqId);
|
|
|
|
setLazyProp<VercelRequestCookies>(req, 'cookies', getCookieParser(req));
|
|
setLazyProp<VercelRequestQuery>(req, 'query', getQueryParser(req));
|
|
setLazyProp<VercelRequestBody>(
|
|
req,
|
|
'body',
|
|
getBodyParser(req, event.body)
|
|
);
|
|
|
|
res.status = statusCode => status(res, statusCode);
|
|
res.redirect = (statusOrUrl, url) => redirect(res, statusOrUrl, url);
|
|
res.send = body => send(req, res, body);
|
|
res.json = jsonBody => json(req, res, jsonBody);
|
|
|
|
try {
|
|
await handler(req, res);
|
|
} catch (err) {
|
|
console.log(`Error from API Route ${req.url}: ${err.stack}`);
|
|
process.exit(1);
|
|
}
|
|
} catch (err) {
|
|
console.log(`Error while handling ${req.url}: ${err.message}`);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
return server;
|
|
}
|