zap
Version:
Lightweight HTTP server framework for Node
257 lines • 9.37 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.fromRequest = exports.redirect = exports.RedirectError = exports.httpError = exports.HttpError = exports.route = exports.router = exports.notFound = exports.send = exports.json = exports.text = exports.buffer = exports.getHeader = exports.serve = void 0;
const content_type_1 = __importDefault(require("content-type"));
const path_to_regexp_1 = require("path-to-regexp");
const raw_body_1 = __importDefault(require("raw-body"));
const stream_1 = require("stream");
const url_1 = require("url");
const IS_DEV = process.env.NODE_ENV === 'development';
function serve(handler, options = {}) {
return async function (req, res) {
const serverRequest = requestFromHTTP(req, options);
const serverResponse = responseFromHTTP(res);
try {
await handler(serverRequest, serverResponse);
}
catch (error) {
if (res.writableEnded)
throw error;
if (error instanceof RedirectError) {
res.statusCode = error.statusCode;
res.setHeader('Location', error.location);
res.end();
return;
}
const errorHandler = options.errorHandler ?? ((_, res, error) => sendError(res, error));
errorHandler(serverRequest, serverResponse, error);
}
};
}
exports.serve = serve;
// Request ---------------------------------------------------------------------
const protocolFromRequest = fromRequest((req, options) => {
const socketProtocol = Boolean(req.socket.encrypted) ? 'https' : 'http';
if (!options.trustProxy)
return socketProtocol;
const headerProtocol = getHeader(req, 'x-forwarded-proto') ?? socketProtocol;
const commaIndex = headerProtocol.indexOf(',');
return commaIndex === -1 ? headerProtocol.trim() : headerProtocol.substring(0, commaIndex).trim();
});
const queryFromRequest = fromRequest((req) => {
return Object.fromEntries(req.parsedURL.searchParams);
});
const urlFromRequest = fromRequest((req) => {
return new url_1.URL(req.url, `${req.protocol}://${req.headers.host}`);
});
function requestFromHTTP(req, options) {
const serverRequest = Object.defineProperties(req, {
protocol: { get: () => protocolFromRequest(serverRequest, options), enumerable: true },
query: { get: () => queryFromRequest(serverRequest), enumerable: true },
parsedURL: { get: () => urlFromRequest(serverRequest), enumerable: true },
});
return serverRequest;
}
function getHeader(req, header) {
const value = req.headers[header];
return Array.isArray(value) ? value[0] : value;
}
exports.getHeader = getHeader;
const requestBodyMap = new WeakMap();
async function buffer(req, { limit = '1mb', encoding } = {}) {
const type = req.headers['content-type'] ?? 'text/plain';
const length = req.headers['content-length'];
if (encoding === undefined) {
encoding = content_type_1.default.parse(type).parameters.charset;
}
const existingBody = requestBodyMap.get(req);
if (existingBody)
return existingBody;
try {
const body = Buffer.from(await (0, raw_body_1.default)(req, { limit, length, encoding }));
requestBodyMap.set(req, body);
return body;
}
catch (error) {
if (error.type === 'entity.too.large') {
throw httpError(413, `Body exceeded ${limit} limit`, error);
}
throw httpError(400, 'Invalid body', error);
}
}
exports.buffer = buffer;
async function text(req, options = {}) {
return await buffer(req, options).then((body) => body.toString());
}
exports.text = text;
async function json(req, options = {}) {
return await text(req, options).then((body) => {
try {
return JSON.parse(body);
}
catch (error) {
throw httpError(400, 'Invalid JSON', error);
}
});
}
exports.json = json;
// Response --------------------------------------------------------------------
function responseFromHTTP(res) {
const serverResponse = Object.defineProperties(res, {});
return serverResponse;
}
function send(res, code, body = null) {
res.statusCode = code;
if (body === null || body === undefined) {
res.end();
return;
}
// Throw errors so they can be handled by the error handler
if (body instanceof Error) {
throw body;
}
if (body instanceof stream_1.Stream || isReadableStream(body)) {
if (!res.getHeader('Content-Type')) {
res.setHeader('Content-Type', 'application/octet-stream');
}
body.pipe(res);
return;
}
if (Buffer.isBuffer(body)) {
if (!res.getHeader('Content-Type')) {
res.setHeader('Content-Type', 'application/octet-stream');
}
res.setHeader('Content-Length', body.length);
res.end(body);
return;
}
let stringifiedBody;
if (typeof body === 'object' || typeof body === 'number') {
stringifiedBody = JSON.stringify(body);
if (!res.getHeader('Content-Type')) {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
}
}
else {
stringifiedBody = body;
}
res.setHeader('Content-Length', Buffer.byteLength(stringifiedBody));
res.end(stringifiedBody);
}
exports.send = send;
function sendError(res, error) {
if (error instanceof HttpError) {
send(res, error.statusCode, error.message);
}
else if (error instanceof Error) {
send(res, 500, IS_DEV ? error.stack : error.message);
}
else {
send(res, 500, `${error}`);
}
}
function notFound() {
return httpError(404, 'Not Found');
}
exports.notFound = notFound;
// Router ----------------------------------------------------------------------
function router(...handlers) {
return async function (req, res) {
for (const current of handlers) {
if (req.method !== current.method)
continue;
const match = current.matchPath(req.parsedURL.pathname);
if (!match)
continue;
req.params = match.params;
return await current(req, res);
}
return send(res, 404, 'Not Found');
};
}
exports.router = router;
// Implementation
function route(method, path, handler) {
const routeHandler = async (req, res) => {
const responseBody = await Promise.resolve(handler(req, res));
if (responseBody === null)
return send(res, 204, null);
if (responseBody === undefined)
return;
send(res, res.statusCode ?? 200, responseBody);
};
return Object.assign(routeHandler, { method, route: path, compilePath: (0, path_to_regexp_1.compile)(path), matchPath: (0, path_to_regexp_1.match)(path) });
}
exports.route = route;
// Errors ----------------------------------------------------------------------
class HttpError extends Error {
constructor(statusCode, message, metadata) {
super(message);
this.statusCode = statusCode;
this.metadata = metadata;
if (Error.captureStackTrace)
Error.captureStackTrace(this, RedirectError);
}
}
exports.HttpError = HttpError;
function httpError(code, message, metadata) {
return new HttpError(code, message, metadata);
}
exports.httpError = httpError;
// Redirects -------------------------------------------------------------------
class RedirectError extends Error {
constructor(statusCode, location) {
super(`Redirect to ${location}, status code ${statusCode}`);
this.statusCode = statusCode;
this.location = location;
if (Error.captureStackTrace)
Error.captureStackTrace(this, RedirectError);
}
}
exports.RedirectError = RedirectError;
function redirect(location, statusCode = 303) {
return new RedirectError(statusCode, location);
}
exports.redirect = redirect;
// Utilities -------------------------------------------------------------------
function isStream(val) {
return val !== null && typeof val === 'object' && typeof val.pipe === 'object';
}
function isReadableStream(val) {
return (isStream(val) &&
val.readable !== false &&
typeof val._read === 'function' &&
typeof val._readableState === 'object');
}
/**
* Creates a function that caches its results for a given request. Both successful responses
* and errors are cached.
*
* @param fn The function that should be cached.
* @returns The results of calling the function
*/
function fromRequest(fn) {
const cache = new WeakMap();
const errorCache = new WeakMap();
const cachedFn = (req, ...rest) => {
if (errorCache.has(req))
throw errorCache.get(req);
if (cache.has(req))
return cache.get(req);
try {
const value = fn(req, ...rest);
cache.set(req, value);
return value;
}
catch (error) {
errorCache.set(req, error);
throw error;
}
};
return cachedFn;
}
exports.fromRequest = fromRequest;
//# sourceMappingURL=zap.js.map
;