UNPKG

20.8 kBJavaScriptView Raw
1import { getQuery as getQuery$1, withoutTrailingSlash, withoutBase } from 'ufo';
2import destr from 'destr';
3import { parse, serialize } from 'cookie-es';
4import { createRouter as createRouter$1 } from 'radix3';
5
6class H3Error extends Error {
7 constructor() {
8 super(...arguments);
9 this.statusCode = 500;
10 this.fatal = false;
11 this.unhandled = false;
12 this.statusMessage = "Internal Server Error";
13 }
14}
15H3Error.__h3_error__ = true;
16function createError(input) {
17 if (typeof input === "string") {
18 return new H3Error(input);
19 }
20 if (isError(input)) {
21 return input;
22 }
23 const err = new H3Error(input.message ?? input.statusMessage, input.cause ? { cause: input.cause } : void 0);
24 if ("stack" in input) {
25 try {
26 Object.defineProperty(err, "stack", { get() {
27 return input.stack;
28 } });
29 } catch {
30 try {
31 err.stack = input.stack;
32 } catch {
33 }
34 }
35 }
36 if (input.statusCode) {
37 err.statusCode = input.statusCode;
38 }
39 if (input.statusMessage) {
40 err.statusMessage = input.statusMessage;
41 }
42 if (input.data) {
43 err.data = input.data;
44 }
45 if (input.fatal !== void 0) {
46 err.fatal = input.fatal;
47 }
48 if (input.unhandled !== void 0) {
49 err.unhandled = input.unhandled;
50 }
51 return err;
52}
53function sendError(event, error, debug) {
54 if (event.res.writableEnded) {
55 return;
56 }
57 const h3Error = isError(error) ? error : createError(error);
58 const responseBody = {
59 statusCode: h3Error.statusCode,
60 statusMessage: h3Error.statusMessage,
61 stack: [],
62 data: h3Error.data
63 };
64 if (debug) {
65 responseBody.stack = (h3Error.stack || "").split("\n").map((l) => l.trim());
66 }
67 if (event.res.writableEnded) {
68 return;
69 }
70 event.res.statusCode = h3Error.statusCode;
71 event.res.statusMessage = h3Error.statusMessage;
72 event.res.setHeader("Content-Type", MIMES.json);
73 event.res.end(JSON.stringify(responseBody, null, 2));
74}
75function isError(input) {
76 return input?.constructor?.__h3_error__ === true;
77}
78
79function getQuery(event) {
80 return getQuery$1(event.req.url || "");
81}
82const useQuery = getQuery;
83function getRouterParams(event) {
84 return event.context.params || {};
85}
86function getRouterParam(event, name) {
87 const params = getRouterParams(event);
88 return params[name];
89}
90function getMethod(event, defaultMethod = "GET") {
91 return (event.req.method || defaultMethod).toUpperCase();
92}
93const useMethod = getMethod;
94function isMethod(event, expected, allowHead) {
95 const method = getMethod(event);
96 if (allowHead && method === "HEAD") {
97 return true;
98 }
99 if (typeof expected === "string") {
100 if (method === expected) {
101 return true;
102 }
103 } else if (expected.includes(method)) {
104 return true;
105 }
106 return false;
107}
108function assertMethod(event, expected, allowHead) {
109 if (!isMethod(event, expected, allowHead)) {
110 throw createError({
111 statusCode: 405,
112 statusMessage: "HTTP method is not allowed."
113 });
114 }
115}
116function getRequestHeaders(event) {
117 return event.req.headers;
118}
119const getHeaders = getRequestHeaders;
120function getRequestHeader(event, name) {
121 const headers = getRequestHeaders(event);
122 const value = headers[name.toLowerCase()];
123 return value;
124}
125const getHeader = getRequestHeader;
126
127const RawBodySymbol = Symbol.for("h3RawBody");
128const ParsedBodySymbol = Symbol.for("h3ParsedBody");
129const PayloadMethods = ["PATCH", "POST", "PUT", "DELETE"];
130function readRawBody(event, encoding = "utf-8") {
131 assertMethod(event, PayloadMethods);
132 if (RawBodySymbol in event.req) {
133 const promise2 = Promise.resolve(event.req[RawBodySymbol]);
134 return encoding ? promise2.then((buff) => buff.toString(encoding)) : promise2;
135 }
136 if ("body" in event.req) {
137 return Promise.resolve(event.req.body);
138 }
139 const promise = event.req[RawBodySymbol] = new Promise((resolve, reject) => {
140 const bodyData = [];
141 event.req.on("error", (err) => {
142 reject(err);
143 }).on("data", (chunk) => {
144 bodyData.push(chunk);
145 }).on("end", () => {
146 resolve(Buffer.concat(bodyData));
147 });
148 });
149 return encoding ? promise.then((buff) => buff.toString(encoding)) : promise;
150}
151const useRawBody = readRawBody;
152async function readBody(event) {
153 if (ParsedBodySymbol in event.req) {
154 return event.req[ParsedBodySymbol];
155 }
156 const body = await readRawBody(event);
157 if (event.req.headers["content-type"] === "application/x-www-form-urlencoded") {
158 const parsedForm = Object.fromEntries(new URLSearchParams(body));
159 return parsedForm;
160 }
161 const json = destr(body);
162 event.req[ParsedBodySymbol] = json;
163 return json;
164}
165const useBody = readBody;
166
167function handleCacheHeaders(event, opts) {
168 const cacheControls = ["public"].concat(opts.cacheControls || []);
169 let cacheMatched = false;
170 if (opts.maxAge !== void 0) {
171 cacheControls.push(`max-age=${+opts.maxAge}`, `s-maxage=${+opts.maxAge}`);
172 }
173 if (opts.modifiedTime) {
174 const modifiedTime = new Date(opts.modifiedTime);
175 const ifModifiedSince = event.req.headers["if-modified-since"];
176 event.res.setHeader("Last-Modified", modifiedTime.toUTCString());
177 if (ifModifiedSince) {
178 if (new Date(ifModifiedSince) >= opts.modifiedTime) {
179 cacheMatched = true;
180 }
181 }
182 }
183 if (opts.etag) {
184 event.res.setHeader("Etag", opts.etag);
185 const ifNonMatch = event.req.headers["if-none-match"];
186 if (ifNonMatch === opts.etag) {
187 cacheMatched = true;
188 }
189 }
190 event.res.setHeader("Cache-Control", cacheControls.join(", "));
191 if (cacheMatched) {
192 event.res.statusCode = 304;
193 event.res.end("");
194 return true;
195 }
196 return false;
197}
198
199const MIMES = {
200 html: "text/html",
201 json: "application/json"
202};
203
204const defer = typeof setImmediate !== "undefined" ? setImmediate : (fn) => fn();
205function send(event, data, type) {
206 if (type) {
207 defaultContentType(event, type);
208 }
209 return new Promise((resolve) => {
210 defer(() => {
211 event.res.end(data);
212 resolve(void 0);
213 });
214 });
215}
216function defaultContentType(event, type) {
217 if (type && !event.res.getHeader("Content-Type")) {
218 event.res.setHeader("Content-Type", type);
219 }
220}
221function sendRedirect(event, location, code = 302) {
222 event.res.statusCode = code;
223 event.res.setHeader("Location", location);
224 const encodedLoc = location.replace(/"/g, "%22");
225 const html = `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`;
226 return send(event, html, MIMES.html);
227}
228function getResponseHeaders(event) {
229 return event.res.getHeaders();
230}
231function getResponseHeader(event, name) {
232 return event.res.getHeader(name);
233}
234function setResponseHeaders(event, headers) {
235 Object.entries(headers).forEach(([name, value]) => event.res.setHeader(name, value));
236}
237const setHeaders = setResponseHeaders;
238function setResponseHeader(event, name, value) {
239 event.res.setHeader(name, value);
240}
241const setHeader = setResponseHeader;
242function appendResponseHeaders(event, headers) {
243 Object.entries(headers).forEach(([name, value]) => appendResponseHeader(event, name, value));
244}
245const appendHeaders = appendResponseHeaders;
246function appendResponseHeader(event, name, value) {
247 let current = event.res.getHeader(name);
248 if (!current) {
249 event.res.setHeader(name, value);
250 return;
251 }
252 if (!Array.isArray(current)) {
253 current = [current.toString()];
254 }
255 event.res.setHeader(name, current.concat(value));
256}
257const appendHeader = appendResponseHeader;
258function isStream(data) {
259 return data && typeof data === "object" && typeof data.pipe === "function" && typeof data.on === "function";
260}
261function sendStream(event, data) {
262 return new Promise((resolve, reject) => {
263 data.pipe(event.res);
264 data.on("end", () => resolve(void 0));
265 data.on("error", (error) => reject(createError(error)));
266 });
267}
268
269function parseCookies(event) {
270 return parse(event.req.headers.cookie || "");
271}
272const useCookies = parseCookies;
273function getCookie(event, name) {
274 return parseCookies(event)[name];
275}
276const useCookie = getCookie;
277function setCookie(event, name, value, serializeOptions) {
278 const cookieStr = serialize(name, value, {
279 path: "/",
280 ...serializeOptions
281 });
282 appendHeader(event, "Set-Cookie", cookieStr);
283}
284function deleteCookie(event, name, serializeOptions) {
285 setCookie(event, name, "", {
286 ...serializeOptions,
287 maxAge: 0
288 });
289}
290
291class H3Headers {
292 constructor(init) {
293 if (!init) {
294 this._headers = {};
295 } else if (Array.isArray(init)) {
296 this._headers = Object.fromEntries(init.map(([key, value]) => [key.toLowerCase(), value]));
297 } else if (init && "append" in init) {
298 this._headers = Object.fromEntries([...init.entries()]);
299 } else {
300 this._headers = Object.fromEntries(Object.entries(init).map(([key, value]) => [key.toLowerCase(), value]));
301 }
302 }
303 append(name, value) {
304 const _name = name.toLowerCase();
305 this.set(_name, [this.get(_name), value].filter(Boolean).join(", "));
306 }
307 delete(name) {
308 delete this._headers[name.toLowerCase()];
309 }
310 get(name) {
311 return this._headers[name.toLowerCase()];
312 }
313 has(name) {
314 return name.toLowerCase() in this._headers;
315 }
316 set(name, value) {
317 this._headers[name.toLowerCase()] = String(value);
318 }
319 forEach(callbackfn) {
320 Object.entries(this._headers).forEach(([key, value]) => callbackfn(value, key, this));
321 }
322}
323
324class H3Response {
325 constructor(body = null, init = {}) {
326 this.body = null;
327 this.type = "default";
328 this.bodyUsed = false;
329 this.headers = new H3Headers(init.headers);
330 this.status = init.status ?? 200;
331 this.statusText = init.statusText || "";
332 this.redirected = !!init.status && [301, 302, 307, 308].includes(init.status);
333 this._body = body;
334 this.url = "";
335 this.ok = this.status < 300 && this.status > 199;
336 }
337 clone() {
338 return new H3Response(this.body, {
339 headers: this.headers,
340 status: this.status,
341 statusText: this.statusText
342 });
343 }
344 arrayBuffer() {
345 return Promise.resolve(this._body);
346 }
347 blob() {
348 return Promise.resolve(this._body);
349 }
350 formData() {
351 return Promise.resolve(this._body);
352 }
353 json() {
354 return Promise.resolve(this._body);
355 }
356 text() {
357 return Promise.resolve(this._body);
358 }
359}
360
361class H3Event {
362 constructor(req, res) {
363 this["__is_event__"] = true;
364 this.context = {};
365 this.req = req;
366 this.res = res;
367 this.event = this;
368 req.event = this;
369 req.context = this.context;
370 req.req = req;
371 req.res = res;
372 res.event = this;
373 res.res = res;
374 res.req = res.req || {};
375 res.req.res = res;
376 res.req.req = req;
377 }
378 respondWith(r) {
379 Promise.resolve(r).then((_response) => {
380 if (this.res.writableEnded) {
381 return;
382 }
383 const response = _response instanceof H3Response ? _response : new H3Response(_response);
384 response.headers.forEach((value, key) => {
385 this.res.setHeader(key, value);
386 });
387 if (response.status) {
388 this.res.statusCode = response.status;
389 }
390 if (response.statusText) {
391 this.res.statusMessage = response.statusText;
392 }
393 if (response.redirected) {
394 this.res.setHeader("Location", response.url);
395 }
396 if (!response._body) {
397 return this.res.end();
398 }
399 if (typeof response._body === "string" || "buffer" in response._body || "byteLength" in response._body) {
400 return this.res.end(response._body);
401 }
402 if (!response.headers.has("content-type")) {
403 response.headers.set("content-type", MIMES.json);
404 }
405 this.res.end(JSON.stringify(response._body));
406 });
407 }
408}
409function isEvent(input) {
410 return "__is_event__" in input;
411}
412function createEvent(req, res) {
413 return new H3Event(req, res);
414}
415
416const defineHandler = (handler) => handler;
417const defineHandle = defineHandler;
418const defineMiddleware = (middleware) => middleware;
419function promisifyHandler(handler) {
420 return function(req, res) {
421 return callHandler(handler, req, res);
422 };
423}
424const promisifyHandle = promisifyHandler;
425function callHandler(handler, req, res) {
426 const isMiddleware = handler.length > 2;
427 return new Promise((resolve, reject) => {
428 const next = (err) => {
429 if (isMiddleware) {
430 res.off("close", next);
431 res.off("error", next);
432 }
433 return err ? reject(createError(err)) : resolve(void 0);
434 };
435 try {
436 const returned = handler(req, res, next);
437 if (isMiddleware && returned === void 0) {
438 res.once("close", next);
439 res.once("error", next);
440 } else {
441 resolve(returned);
442 }
443 } catch (err) {
444 next(err);
445 }
446 });
447}
448function defineLazyHandler(handler, promisify) {
449 let _promise;
450 const resolve = () => {
451 if (!_promise) {
452 _promise = Promise.resolve(handler()).then((r) => promisify ? promisifyHandler(r.default || r) : r.default || r);
453 }
454 return _promise;
455 };
456 return function(req, res) {
457 return resolve().then((h) => h(req, res));
458 };
459}
460const lazyHandle = defineLazyHandler;
461function useBase(base, handler) {
462 base = withoutTrailingSlash(base);
463 if (!base) {
464 return handler;
465 }
466 return function(req, res) {
467 req.originalUrl = req.originalUrl || req.url || "/";
468 req.url = withoutBase(req.url || "/", base);
469 return handler(req, res);
470 };
471}
472
473function defineEventHandler(handler) {
474 handler.__is_handler__ = true;
475 return handler;
476}
477const eventHandler = defineEventHandler;
478function isEventHandler(input) {
479 return "__is_handler__" in input;
480}
481function toEventHandler(handler) {
482 if (isEventHandler(handler)) {
483 return handler;
484 }
485 if (typeof handler !== "function") {
486 throw new TypeError("Invalid handler. It should be a function:", handler);
487 }
488 return eventHandler((event) => {
489 return callHandler(handler, event.req, event.res);
490 });
491}
492function dynamicEventHandler(initial) {
493 let current = initial;
494 const wrapper = eventHandler((event) => {
495 if (current) {
496 return current(event);
497 }
498 });
499 wrapper.set = (handler) => {
500 current = handler;
501 };
502 return wrapper;
503}
504function defineLazyEventHandler(factory) {
505 let _promise;
506 let _resolved;
507 const resolveHandler = () => {
508 if (_resolved) {
509 return Promise.resolve(_resolved);
510 }
511 if (!_promise) {
512 _promise = Promise.resolve(factory()).then((r) => {
513 const handler = r.default || r;
514 if (typeof handler !== "function") {
515 throw new TypeError("Invalid lazy handler result. It should be a function:", handler);
516 }
517 _resolved = toEventHandler(r.default || r);
518 return _resolved;
519 });
520 }
521 return _promise;
522 };
523 return eventHandler((event) => {
524 if (_resolved) {
525 return _resolved(event);
526 }
527 return resolveHandler().then((handler) => handler(event));
528 });
529}
530const lazyEventHandler = defineLazyEventHandler;
531
532function createApp(options = {}) {
533 const stack = [];
534 const handler = createAppEventHandler(stack, options);
535 const nodeHandler = async function(req, res) {
536 const event = createEvent(req, res);
537 try {
538 await handler(event);
539 } catch (_error) {
540 const error = createError(_error);
541 if (!isError(_error)) {
542 error.unhandled = true;
543 }
544 if (options.onError) {
545 await options.onError(error, event);
546 } else {
547 if (error.unhandled || error.fatal) {
548 console.error("[h3]", error.fatal ? "[fatal]" : "[unhandled]", error);
549 }
550 await sendError(event, error, !!options.debug);
551 }
552 }
553 };
554 const app = nodeHandler;
555 app.nodeHandler = nodeHandler;
556 app.stack = stack;
557 app.handler = handler;
558 app.use = (arg1, arg2, arg3) => use(app, arg1, arg2, arg3);
559 return app;
560}
561function use(app, arg1, arg2, arg3) {
562 if (Array.isArray(arg1)) {
563 arg1.forEach((i) => use(app, i, arg2, arg3));
564 } else if (Array.isArray(arg2)) {
565 arg2.forEach((i) => use(app, arg1, i, arg3));
566 } else if (typeof arg1 === "string") {
567 app.stack.push(normalizeLayer({ ...arg3, route: arg1, handler: arg2 }));
568 } else if (typeof arg1 === "function") {
569 app.stack.push(normalizeLayer({ ...arg2, route: "/", handler: arg1 }));
570 } else {
571 app.stack.push(normalizeLayer({ ...arg1 }));
572 }
573 return app;
574}
575function createAppEventHandler(stack, options) {
576 const spacing = options.debug ? 2 : void 0;
577 return eventHandler(async (event) => {
578 event.req.originalUrl = event.req.originalUrl || event.req.url || "/";
579 const reqUrl = event.req.url || "/";
580 for (const layer of stack) {
581 if (layer.route.length > 1) {
582 if (!reqUrl.startsWith(layer.route)) {
583 continue;
584 }
585 event.req.url = reqUrl.slice(layer.route.length) || "/";
586 } else {
587 event.req.url = reqUrl;
588 }
589 if (layer.match && !layer.match(event.req.url, event)) {
590 continue;
591 }
592 const val = await layer.handler(event);
593 if (event.res.writableEnded) {
594 return;
595 }
596 const type = typeof val;
597 if (type === "string") {
598 return send(event, val, MIMES.html);
599 } else if (isStream(val)) {
600 return sendStream(event, val);
601 } else if (val === null) {
602 event.res.statusCode = 204;
603 return send(event);
604 } else if (type === "object" || type === "boolean" || type === "number") {
605 if (val.buffer) {
606 return send(event, val);
607 } else if (val instanceof Error) {
608 throw createError(val);
609 } else {
610 return send(event, JSON.stringify(val, null, spacing), MIMES.json);
611 }
612 }
613 }
614 if (!event.res.writableEnded) {
615 throw createError({ statusCode: 404, statusMessage: "Not Found" });
616 }
617 });
618}
619function normalizeLayer(input) {
620 let handler = input.handler || input.handle;
621 if (handler.handler) {
622 handler = handler.handler;
623 }
624 if (input.lazy) {
625 handler = lazyEventHandler(handler);
626 } else if (!isEventHandler(handler)) {
627 handler = toEventHandler(handler);
628 }
629 return {
630 route: withoutTrailingSlash(input.route),
631 match: input.match,
632 handler
633 };
634}
635
636const RouterMethods = ["connect", "delete", "get", "head", "options", "post", "put", "trace", "patch"];
637function createRouter() {
638 const _router = createRouter$1({});
639 const routes = {};
640 const router = {};
641 const addRoute = (path, handler, method) => {
642 let route = routes[path];
643 if (!route) {
644 routes[path] = route = { handlers: {} };
645 _router.insert(path, route);
646 }
647 if (Array.isArray(method)) {
648 method.forEach((m) => addRoute(path, handler, m));
649 } else {
650 route.handlers[method] = toEventHandler(handler);
651 }
652 return router;
653 };
654 router.use = router.add = (path, handler, method) => addRoute(path, handler, method || "all");
655 for (const method of RouterMethods) {
656 router[method] = (path, handle) => router.add(path, handle, method);
657 }
658 router.handler = eventHandler((event) => {
659 let path = event.req.url || "/";
660 const queryUrlIndex = path.lastIndexOf("?");
661 if (queryUrlIndex > -1) {
662 path = path.substring(0, queryUrlIndex);
663 }
664 const matched = _router.lookup(path);
665 if (!matched) {
666 throw createError({
667 statusCode: 404,
668 name: "Not Found",
669 statusMessage: `Cannot find any route matching ${event.req.url || "/"}.`
670 });
671 }
672 const method = (event.req.method || "get").toLowerCase();
673 const handler = matched.handlers[method] || matched.handlers.all;
674 if (!handler) {
675 throw createError({
676 statusCode: 405,
677 name: "Method Not Allowed",
678 statusMessage: `Method ${method} is not allowed on this route.`
679 });
680 }
681 const params = matched.params || {};
682 event.event.context.params = params;
683 event.req.context.params = params;
684 return handler(event);
685 });
686 return router;
687}
688
689export { H3Error, H3Event, H3Headers, H3Response, MIMES, appendHeader, appendHeaders, appendResponseHeader, appendResponseHeaders, assertMethod, callHandler, createApp, createAppEventHandler, createError, createEvent, createRouter, defaultContentType, defineEventHandler, defineHandle, defineHandler, defineLazyEventHandler, defineLazyHandler, defineMiddleware, deleteCookie, dynamicEventHandler, eventHandler, getCookie, getHeader, getHeaders, getMethod, getQuery, getRequestHeader, getRequestHeaders, getResponseHeader, getResponseHeaders, getRouterParam, getRouterParams, handleCacheHeaders, isError, isEvent, isEventHandler, isMethod, isStream, lazyEventHandler, lazyHandle, parseCookies, promisifyHandle, promisifyHandler, readBody, readRawBody, send, sendError, sendRedirect, sendStream, setCookie, setHeader, setHeaders, setResponseHeader, setResponseHeaders, toEventHandler, use, useBase, useBody, useCookie, useCookies, useMethod, useQuery, useRawBody };