UNPKG

9.37 kBJavaScriptView Raw
1"use strict";
2var __importDefault = (this && this.__importDefault) || function (mod) {
3 return (mod && mod.__esModule) ? mod : { "default": mod };
4};
5Object.defineProperty(exports, "__esModule", { value: true });
6exports.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;
7const content_type_1 = __importDefault(require("content-type"));
8const path_to_regexp_1 = require("path-to-regexp");
9const raw_body_1 = __importDefault(require("raw-body"));
10const stream_1 = require("stream");
11const url_1 = require("url");
12const IS_DEV = process.env.NODE_ENV === 'development';
13function serve(handler, options = {}) {
14 return async function (req, res) {
15 const serverRequest = requestFromHTTP(req, options);
16 const serverResponse = responseFromHTTP(res);
17 try {
18 await handler(serverRequest, serverResponse);
19 }
20 catch (error) {
21 if (res.writableEnded)
22 throw error;
23 if (error instanceof RedirectError) {
24 res.statusCode = error.statusCode;
25 res.setHeader('Location', error.location);
26 res.end();
27 return;
28 }
29 const errorHandler = options.errorHandler ?? ((_, res, error) => sendError(res, error));
30 errorHandler(serverRequest, serverResponse, error);
31 }
32 };
33}
34exports.serve = serve;
35// Request ---------------------------------------------------------------------
36const protocolFromRequest = fromRequest((req, options) => {
37 const socketProtocol = Boolean(req.socket.encrypted) ? 'https' : 'http';
38 if (!options.trustProxy)
39 return socketProtocol;
40 const headerProtocol = getHeader(req, 'x-forwarded-proto') ?? socketProtocol;
41 const commaIndex = headerProtocol.indexOf(',');
42 return commaIndex === -1 ? headerProtocol.trim() : headerProtocol.substring(0, commaIndex).trim();
43});
44const queryFromRequest = fromRequest((req) => {
45 return Object.fromEntries(req.parsedURL.searchParams);
46});
47const urlFromRequest = fromRequest((req) => {
48 return new url_1.URL(req.url, `${req.protocol}://${req.headers.host}`);
49});
50function requestFromHTTP(req, options) {
51 const serverRequest = Object.defineProperties(req, {
52 protocol: { get: () => protocolFromRequest(serverRequest, options), enumerable: true },
53 query: { get: () => queryFromRequest(serverRequest), enumerable: true },
54 parsedURL: { get: () => urlFromRequest(serverRequest), enumerable: true },
55 });
56 return serverRequest;
57}
58function getHeader(req, header) {
59 const value = req.headers[header];
60 return Array.isArray(value) ? value[0] : value;
61}
62exports.getHeader = getHeader;
63const requestBodyMap = new WeakMap();
64async function buffer(req, { limit = '1mb', encoding } = {}) {
65 const type = req.headers['content-type'] ?? 'text/plain';
66 const length = req.headers['content-length'];
67 if (encoding === undefined) {
68 encoding = content_type_1.default.parse(type).parameters.charset;
69 }
70 const existingBody = requestBodyMap.get(req);
71 if (existingBody)
72 return existingBody;
73 try {
74 const body = Buffer.from(await (0, raw_body_1.default)(req, { limit, length, encoding }));
75 requestBodyMap.set(req, body);
76 return body;
77 }
78 catch (error) {
79 if (error.type === 'entity.too.large') {
80 throw httpError(413, `Body exceeded ${limit} limit`, error);
81 }
82 throw httpError(400, 'Invalid body', error);
83 }
84}
85exports.buffer = buffer;
86async function text(req, options = {}) {
87 return await buffer(req, options).then((body) => body.toString());
88}
89exports.text = text;
90async function json(req, options = {}) {
91 return await text(req, options).then((body) => {
92 try {
93 return JSON.parse(body);
94 }
95 catch (error) {
96 throw httpError(400, 'Invalid JSON', error);
97 }
98 });
99}
100exports.json = json;
101// Response --------------------------------------------------------------------
102function responseFromHTTP(res) {
103 const serverResponse = Object.defineProperties(res, {});
104 return serverResponse;
105}
106function send(res, code, body = null) {
107 res.statusCode = code;
108 if (body === null || body === undefined) {
109 res.end();
110 return;
111 }
112 // Throw errors so they can be handled by the error handler
113 if (body instanceof Error) {
114 throw body;
115 }
116 if (body instanceof stream_1.Stream || isReadableStream(body)) {
117 if (!res.getHeader('Content-Type')) {
118 res.setHeader('Content-Type', 'application/octet-stream');
119 }
120 body.pipe(res);
121 return;
122 }
123 if (Buffer.isBuffer(body)) {
124 if (!res.getHeader('Content-Type')) {
125 res.setHeader('Content-Type', 'application/octet-stream');
126 }
127 res.setHeader('Content-Length', body.length);
128 res.end(body);
129 return;
130 }
131 let stringifiedBody;
132 if (typeof body === 'object' || typeof body === 'number') {
133 stringifiedBody = JSON.stringify(body);
134 if (!res.getHeader('Content-Type')) {
135 res.setHeader('Content-Type', 'application/json; charset=utf-8');
136 }
137 }
138 else {
139 stringifiedBody = body;
140 }
141 res.setHeader('Content-Length', Buffer.byteLength(stringifiedBody));
142 res.end(stringifiedBody);
143}
144exports.send = send;
145function sendError(res, error) {
146 if (error instanceof HttpError) {
147 send(res, error.statusCode, error.message);
148 }
149 else if (error instanceof Error) {
150 send(res, 500, IS_DEV ? error.stack : error.message);
151 }
152 else {
153 send(res, 500, `${error}`);
154 }
155}
156function notFound() {
157 return httpError(404, 'Not Found');
158}
159exports.notFound = notFound;
160// Router ----------------------------------------------------------------------
161function router(...handlers) {
162 return async function (req, res) {
163 for (const current of handlers) {
164 if (req.method !== current.method)
165 continue;
166 const match = current.matchPath(req.parsedURL.pathname);
167 if (!match)
168 continue;
169 req.params = match.params;
170 return await current(req, res);
171 }
172 return send(res, 404, 'Not Found');
173 };
174}
175exports.router = router;
176// Implementation
177function route(method, path, handler) {
178 const routeHandler = async (req, res) => {
179 const responseBody = await Promise.resolve(handler(req, res));
180 if (responseBody === null)
181 return send(res, 204, null);
182 if (responseBody === undefined)
183 return;
184 send(res, res.statusCode ?? 200, responseBody);
185 };
186 return Object.assign(routeHandler, { method, route: path, compilePath: (0, path_to_regexp_1.compile)(path), matchPath: (0, path_to_regexp_1.match)(path) });
187}
188exports.route = route;
189// Errors ----------------------------------------------------------------------
190class HttpError extends Error {
191 constructor(statusCode, message, metadata) {
192 super(message);
193 this.statusCode = statusCode;
194 this.metadata = metadata;
195 if (Error.captureStackTrace)
196 Error.captureStackTrace(this, RedirectError);
197 }
198}
199exports.HttpError = HttpError;
200function httpError(code, message, metadata) {
201 return new HttpError(code, message, metadata);
202}
203exports.httpError = httpError;
204// Redirects -------------------------------------------------------------------
205class RedirectError extends Error {
206 constructor(statusCode, location) {
207 super(`Redirect to ${location}, status code ${statusCode}`);
208 this.statusCode = statusCode;
209 this.location = location;
210 if (Error.captureStackTrace)
211 Error.captureStackTrace(this, RedirectError);
212 }
213}
214exports.RedirectError = RedirectError;
215function redirect(location, statusCode = 303) {
216 return new RedirectError(statusCode, location);
217}
218exports.redirect = redirect;
219// Utilities -------------------------------------------------------------------
220function isStream(val) {
221 return val !== null && typeof val === 'object' && typeof val.pipe === 'object';
222}
223function isReadableStream(val) {
224 return (isStream(val) &&
225 val.readable !== false &&
226 typeof val._read === 'function' &&
227 typeof val._readableState === 'object');
228}
229/**
230 * Creates a function that caches its results for a given request. Both successful responses
231 * and errors are cached.
232 *
233 * @param fn The function that should be cached.
234 * @returns The results of calling the function
235 */
236function fromRequest(fn) {
237 const cache = new WeakMap();
238 const errorCache = new WeakMap();
239 const cachedFn = (req, ...rest) => {
240 if (errorCache.has(req))
241 throw errorCache.get(req);
242 if (cache.has(req))
243 return cache.get(req);
244 try {
245 const value = fn(req, ...rest);
246 cache.set(req, value);
247 return value;
248 }
249 catch (error) {
250 errorCache.set(req, error);
251 throw error;
252 }
253 };
254 return cachedFn;
255}
256exports.fromRequest = fromRequest;
257//# sourceMappingURL=zap.js.map
\No newline at end of file