UNPKG

zap

Version:

Lightweight HTTP server framework for Node

257 lines 9.37 kB
"use strict"; 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