ultimate-express 2.0.9 → 2.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/router.js CHANGED
@@ -1,658 +1,659 @@
1
- /*
2
- Copyright 2024 dimden.dev
3
-
4
- Licensed under the Apache License, Version 2.0 (the "License");
5
- you may not use this file except in compliance with the License.
6
- You may obtain a copy of the License at
7
-
8
- http://www.apache.org/licenses/LICENSE-2.0
9
-
10
- Unless required by applicable law or agreed to in writing, software
11
- distributed under the License is distributed on an "AS IS" BASIS,
12
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- See the License for the specific language governing permissions and
14
- limitations under the License.
15
- */
16
-
17
- const { patternToRegex, needsConversionToRegex, deprecated, findIndexStartingFrom, canBeOptimized, NullObject, EMPTY_REGEX } = require("./utils.js");
18
- const Response = require("./response.js");
19
- const Request = require("./request.js");
20
- const { EventEmitter } = require("tseep");
21
- const compileDeclarative = require("./declarative.js");
22
- const statuses = require("statuses");
23
-
24
- let resCodes = {}, resDecMethods = ['set', 'setHeader', 'header', 'send', 'end', 'append', 'status'];
25
- for(let method of resDecMethods) {
26
- resCodes[method] = Response.prototype[method].toString();
27
- }
28
-
29
- let routeKey = 0;
30
-
31
- const methods = [
32
- 'all',
33
- 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace', 'connect',
34
- 'checkout', 'copy', 'lock', 'mkcol', 'move', 'purge', 'propfind', 'proppatch',
35
- 'search', 'subscribe', 'unsubscribe', 'report', 'mkactivity', 'mkcalendar',
36
- 'checkout', 'merge', 'm-search', 'notify', 'subscribe', 'unsubscribe', 'search'
37
- ];
38
- const supportedUwsMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE'];
39
-
40
- const regExParam = /:(\w+)/g;
41
-
42
- module.exports = class Router extends EventEmitter {
43
- constructor(settings = {}) {
44
- super();
45
-
46
- this._paramCallbacks = new Map();
47
- this._mountpathCache = new Map();
48
- this._routes = [];
49
- this.mountpath = '/';
50
- this.settings = settings;
51
- this._request = Request;
52
- this._response = Response;
53
- this.request = this._request.prototype;
54
- this.response = this._response.prototype;
55
-
56
- if(typeof settings.caseSensitive !== 'undefined') {
57
- this.settings['case sensitive routing'] = settings.caseSensitive;
58
- delete this.settings.caseSensitive;
59
- }
60
- if(typeof settings.strict !== 'undefined') {
61
- this.settings['strict routing'] = settings.strict;
62
- delete this.settings.strict;
63
- }
64
-
65
- if(typeof this.settings['case sensitive routing'] === 'undefined') {
66
- this.settings['case sensitive routing'] = true;
67
- }
68
-
69
- for(let method of methods) {
70
- this[method] = (path, ...callbacks) => {
71
- return this.createRoute(method.toUpperCase(), path, this, ...callbacks);
72
- };
73
- };
74
- }
75
-
76
- get(path, ...callbacks) {
77
- if(typeof path === 'string' && callbacks.length === 0) {
78
- const key = path;
79
- const res = this.settings[key];
80
- if(typeof res === 'undefined' && this.parent) {
81
- return this.parent.get(key);
82
- } else {
83
- return res;
84
- }
85
- }
86
- return this.createRoute('GET', path, this, ...callbacks);
87
- }
88
-
89
- del(path, ...callbacks) {
90
- deprecated('app.del', 'app.delete');
91
- return this.createRoute('DELETE', path, this, ...callbacks);
92
- }
93
-
94
- getFullMountpath(req) {
95
- let fullStack = req._stack.join("");
96
- if(!fullStack){
97
- return EMPTY_REGEX;
98
- }
99
- let fullMountpath = this._mountpathCache.get(fullStack);
100
- if(!fullMountpath) {
101
- fullMountpath = patternToRegex(fullStack, true);
102
- this._mountpathCache.set(fullStack, fullMountpath);
103
- }
104
- return fullMountpath;
105
- }
106
-
107
- _pathMatches(route, req) {
108
- let path = req._opPath;
109
- let pattern = route.pattern;
110
-
111
- if(req.endsWithSlash && path.endsWith('/') && !this.get('strict routing')) {
112
- path = path.slice(0, -1);
113
- }
114
-
115
- if (typeof pattern === 'string') {
116
- if(pattern === '/*') {
117
- return true;
118
- }
119
- if(path === '') {
120
- path = '/';
121
- }
122
- if(!this.get('case sensitive routing')) {
123
- path = path.toLowerCase();
124
- pattern = pattern.toLowerCase();
125
- }
126
- return pattern === path;
127
- }
128
- if (pattern === EMPTY_REGEX){
129
- return true;
130
- }
131
- return pattern.test(path);
132
- }
133
-
134
- createRoute(method, path, parent = this, ...callbacks) {
135
- callbacks = callbacks.flat();
136
- const paths = Array.isArray(path) ? path : [path];
137
- const routes = [];
138
- for(let path of paths) {
139
- if(!this.get('strict routing') && typeof path === 'string' && path.endsWith('/') && path !== '/') {
140
- path = path.slice(0, -1);
141
- }
142
- if(path === '*') {
143
- path = '/*';
144
- }
145
- const route = {
146
- method: method === 'USE' ? 'ALL' : method.toUpperCase(),
147
- path,
148
- pattern: method === 'USE' || needsConversionToRegex(path) ? patternToRegex(path, method === 'USE') : path,
149
- callbacks,
150
- routeKey: routeKey++,
151
- use: method === 'USE',
152
- all: method === 'ALL' || method === 'USE',
153
- gettable: method === 'GET' || method === 'HEAD',
154
- };
155
- if(typeof route.path === 'string' && (route.path.includes(':') || route.path.includes('*') || (route.path.includes('(') && route.path.includes(')'))) && route.pattern instanceof RegExp) {
156
- route.complex = true;
157
- }
158
- routes.push(route);
159
- // normal routes optimization
160
- if(canBeOptimized(route.path) && route.pattern !== '/*' && !this.parent && this.get('case sensitive routing') && this.uwsApp) {
161
- if(supportedUwsMethods.includes(method)) {
162
- const optimizedPath = this._optimizeRoute(route, this._routes);
163
- if(optimizedPath) {
164
- this._registerUwsRoute(route, optimizedPath);
165
- }
166
- }
167
- }
168
- // router optimization
169
- if(
170
- route.use && !needsConversionToRegex(path) && path !== '/*' && // must be predictable path
171
- this.get('case sensitive routing') && // uWS only supports case sensitive routing
172
- callbacks.filter(c => c instanceof Router).length === 1 && // only 1 router can be optimized per route
173
- callbacks[callbacks.length - 1] instanceof Router // the router must be the last callback
174
- ) {
175
- let callbacksBeforeRouter = [];
176
- for(let callback of callbacks) {
177
- if(callback instanceof Router) {
178
- // get optimized path to router
179
- let optimizedPathToRouter = this._optimizeRoute(route, this._routes);
180
- if(!optimizedPathToRouter) {
181
- break;
182
- }
183
- optimizedPathToRouter = optimizedPathToRouter.slice(0, -1); // remove last element, which is the router itself
184
- if(optimizedPathToRouter) {
185
- // wait for routes in router to be registered
186
- const t = setTimeout(() => {
187
- if(!this.listenCalled) {
188
- return; // can only optimize router whos parent is listening
189
- }
190
- for(let cbroute of callback._routes) {
191
- if(!needsConversionToRegex(cbroute.path) && cbroute.path !== '/*' && supportedUwsMethods.includes(cbroute.method)) {
192
- let optimizedRouterPath = this._optimizeRoute(cbroute, callback._routes);
193
- if(optimizedRouterPath) {
194
- optimizedRouterPath = optimizedRouterPath.slice(0, -1);
195
- const optimizedPath = [...optimizedPathToRouter, {
196
- // fake route to update req._opPath and req.url
197
- ...route,
198
- callbacks: [
199
- (req, res, next) => {
200
- next('skipPop');
201
- }
202
- ]
203
- }, ...optimizedRouterPath];
204
- this._registerUwsRoute({
205
- ...cbroute,
206
- path: route.path + cbroute.path,
207
- pattern: route.path + cbroute.path,
208
- optimizedRouter: true
209
- }, optimizedPath);
210
- }
211
- }
212
- }
213
- }, 100);
214
- t.unref();
215
- }
216
- // only 1 router can be optimized per route
217
- break;
218
- } else {
219
- callbacksBeforeRouter.push(route);
220
- }
221
- }
222
- }
223
- }
224
- this._routes.push(...routes);
225
-
226
- return parent;
227
- }
228
-
229
- // if route is a simple string, its possible to pre-calculate its path
230
- // and then create a native uWS route for it, which is much faster
231
- _optimizeRoute(route, routes) {
232
- const optimizedPath = [];
233
-
234
- for(let i = 0; i < routes.length; i++) {
235
- const r = routes[i];
236
- if(r.routeKey > route.routeKey) {
237
- break;
238
- }
239
- // if the methods are not the same, and its not an all method, skip it
240
- if(!r.all && r.method !== route.method) {
241
- // check if the methods are compatible (GET and HEAD)
242
- if(!(r.method === 'HEAD' && route.method === 'GET')) {
243
- continue;
244
- }
245
- }
246
-
247
- // check if the paths match
248
- if(
249
- (r.pattern instanceof RegExp && r.pattern.test(route.path)) ||
250
- (typeof r.pattern === 'string' && (r.pattern === route.path || r.pattern === '/*'))
251
- ) {
252
- if(r.callbacks.some(c => c instanceof Router)) {
253
- return false; // cant optimize nested routers with matches
254
- }
255
- optimizedPath.push(r);
256
- }
257
- }
258
- optimizedPath.push(route);
259
-
260
- return optimizedPath;
261
- }
262
-
263
- handleRequest(res, req) {
264
- const request = new this._request(req, res, this);
265
- const response = new this._response(res, request, this);
266
- request.res = response;
267
- response.req = request;
268
- res.onAborted(() => {
269
- const err = new Error('Connection closed');
270
- err.code = 'ECONNRESET';
271
- response.aborted = true;
272
- response.finished = true;
273
- response.socket?.emit('error', err);
274
- });
275
-
276
- return { request, response };
277
- }
278
-
279
- _registerUwsRoute(route, optimizedPath) {
280
- let method = route.method.toLowerCase();
281
- if(method === 'all') {
282
- method = 'any';
283
- } else if(method === 'delete') {
284
- method = 'del';
285
- }
286
- if(!route.optimizedRouter && route.path.includes(":")) {
287
- route.optimizedParams = route.path.match(regExParam).map(p => p.slice(1));
288
- }
289
- let fn = async (res, req) => {
290
- const { request, response } = this.handleRequest(res, req);
291
- if(route.optimizedParams) {
292
- request.optimizedParams = new NullObject();
293
- for(let i = 0; i < route.optimizedParams.length; i++) {
294
- request.optimizedParams[route.optimizedParams[i]] = req.getParameter(i);
295
- }
296
- }
297
- const matchedRoute = await this._routeRequest(request, response, 0, optimizedPath, true, route);
298
- if(!matchedRoute && !response.headersSent && !response.aborted) {
299
- if(request._error) {
300
- return this._handleError(request._error, null, request, response);
301
- }
302
- if(request._isOptions && request._matchedMethods.size > 0) {
303
- const allowedMethods = Array.from(request._matchedMethods).join(',');
304
- response.setHeader('Allow', allowedMethods);
305
- response.send(allowedMethods);
306
- return;
307
- }
308
- response.status(404);
309
- request.noEtag = true;
310
- this._sendErrorPage(request, response, `Cannot ${request.method} ${request._originalPath}`, false);
311
- }
312
- };
313
- route.optimizedPath = optimizedPath;
314
-
315
- let replacedPath = route.path;
316
- const realFn = fn;
317
-
318
- // check if route is declarative
319
- if(
320
- optimizedPath.length === 1 && // must not have middlewares
321
- route.callbacks.length === 1 && // must not have multiple callbacks
322
- typeof route.callbacks[0] === 'function' && // must be a function
323
- this._paramCallbacks.size === 0 && // app.param() is not supported
324
- !resDecMethods.some(method => resCodes[method] !== this.response[method].toString()) && // must not have injected methods
325
- this.get('declarative responses') // must have declarative responses enabled
326
- ) {
327
- const decRes = compileDeclarative(route.callbacks[0], this);
328
- if(decRes) {
329
- fn = decRes;
330
- }
331
- } else {
332
- replacedPath = route.path.replace(regExParam, ':x');
333
- }
334
-
335
- this.uwsApp[method](replacedPath, fn);
336
- if(!this.get('strict routing') && route.path[route.path.length - 1] !== '/') {
337
- this.uwsApp[method](replacedPath + '/', fn);
338
- if(method === 'get') {
339
- this.uwsApp.head(replacedPath + '/', realFn);
340
- }
341
- }
342
- if(method === 'get') {
343
- this.uwsApp.head(replacedPath, realFn);
344
- }
345
- }
346
-
347
- _handleError(err, handler, request, response) {
348
- if(handler) {
349
- return handler(err, request, response, () => {
350
- delete request._error;
351
- delete request._errorKey;
352
- return request.next();
353
- });
354
- }
355
- console.error(err);
356
- if(response.statusCode === 200) {
357
- response.statusCode = 500;
358
- }
359
- this._sendErrorPage(request, response, err, true);
360
- }
361
-
362
- _extractParams(pattern, path) {
363
- if(path.endsWith('/')) {
364
- path = path.slice(0, -1);
365
- }
366
- let match = pattern.exec(path);
367
- if( match?.groups ){
368
- return match.groups;
369
- }
370
- const obj = new NullObject();
371
- for(let i = 1; i < match.length; i++) {
372
- obj[i - 1] = match[i];
373
- }
374
- return obj;
375
- }
376
-
377
- _preprocessRequest(req, res, route) {
378
- req.route = route;
379
- if(route.optimizedParams) {
380
- req.params = {...req.optimizedParams};
381
- } else if(route.complex) {
382
- let path = req._originalPath;
383
- if(req._stack.length > 0) {
384
- path = path.replace(this.getFullMountpath(req), '');
385
- }
386
- req.params = {...this._extractParams(route.pattern, path)};
387
- if(req._paramStack.length > 0) {
388
- for(let params of req._paramStack) {
389
- req.params = {...params, ...req.params};
390
- }
391
- }
392
- } else {
393
- req.params = {};
394
- if(req._paramStack.length > 0) {
395
- for(let params of req._paramStack) {
396
- req.params = {...params, ...req.params};
397
- }
398
- }
399
- }
400
-
401
- if(this._paramCallbacks.size > 0) {
402
- return new Promise(async resolve => {
403
- for(let param in req.params) {
404
- const pcs = this._paramCallbacks.get(param);
405
- if(pcs && !req._gotParams.has(param)) {
406
- req._gotParams.add(param);
407
- for(let i = 0, len = pcs.length; i < len; i++) {
408
- const fn = pcs[i];
409
- await new Promise(resolveRoute => {
410
- const next = (thingamabob) => {
411
- if(thingamabob) {
412
- if(thingamabob === 'route') {
413
- return resolve('route');
414
- } else {
415
- req._error = thingamabob;
416
- req._errorKey = route.routeKey;
417
- }
418
- }
419
- return resolveRoute();
420
- };
421
- req.next = next;
422
- fn(req, res, next, req.params[param], param);
423
- });
424
- }
425
- }
426
- }
427
-
428
- resolve(true)
429
- });
430
- }
431
- return true;
432
- }
433
-
434
- param(name, fn) {
435
- if(typeof name === 'function') {
436
- deprecated('app.param(callback)', 'app.param(name, callback)', true);
437
- this._paramFunction = name;
438
- } else {
439
- if(this._paramFunction) {
440
- if(!this._paramCallbacks.has(name)) {
441
- this._paramCallbacks.set(name, []);
442
- }
443
- this._paramCallbacks.get(name).push(this._paramFunction(name, fn));
444
- } else {
445
- let names = Array.isArray(name) ? name : [name];
446
- for(let name of names) {
447
- if(!this._paramCallbacks.has(name)) {
448
- this._paramCallbacks.set(name, []);
449
- }
450
- this._paramCallbacks.get(name).push(fn);
451
- }
452
- }
453
- }
454
- }
455
-
456
- async _routeRequest(req, res, startIndex = 0, routes = this._routes, skipCheck = false, skipUntil) {
457
- let routeIndex = skipCheck ? startIndex : findIndexStartingFrom(routes, r => (r.all || r.method === req.method || req._isOptions || (r.gettable && req._isHead)) && this._pathMatches(r, req), startIndex);
458
- const route = routes[routeIndex];
459
- if(!route) {
460
- if(!skipCheck) {
461
- // on normal unoptimized routes, if theres no match then there is no route
462
- return false;
463
- }
464
- // on optimized routes, there can be more routes, so we have to use unoptimized routing and skip until we find route we stopped at
465
- return this._routeRequest(req, res, 0, this._routes, false, skipUntil);
466
- }
467
- let callbackindex = 0;
468
-
469
- // avoid calling _preprocessRequest as async function as its slower
470
- // but it seems like calling it as async has unintended, but useful consequence of resetting max call stack size
471
- // so call it as async when the request has been through every 300 routes to reset it
472
- const continueRoute = this._paramCallbacks.size === 0 && req.routeCount % 300 !== 0 ?
473
- this._preprocessRequest(req, res, route) : await this._preprocessRequest(req, res, route);
474
-
475
- const strictRouting = this.get('strict routing');
476
- if(route.use) {
477
- req._stack.push(route.path);
478
- const fullMountpath = this.getFullMountpath(req);
479
- req._opPath = fullMountpath !== EMPTY_REGEX ? req._originalPath.replace(fullMountpath, '') : req._originalPath;
480
- if(req.endsWithSlash && req._opPath[req._opPath.length - 1] !== '/') {
481
- if(strictRouting) {
482
- req._opPath += '/';
483
- } else {
484
- req._opPath = req._opPath.slice(0, -1);
485
- }
486
- }
487
- req.url = req._opPath + req.urlQuery;
488
- req.path = req._opPath;
489
- if(req._opPath === '') {
490
- req.url = '/';
491
- req.path = '/';
492
- }
493
- }
494
- return new Promise((resolve) => {
495
- const next = async (thingamabob) => {
496
- if(thingamabob) {
497
- if(thingamabob === 'route' || thingamabob === 'skipPop') {
498
- if(route.use && thingamabob !== 'skipPop') {
499
- req._stack.pop();
500
-
501
- req._opPath = req._stack.length > 0 ? req._originalPath.replace(this.getFullMountpath(req), '') : req._originalPath;
502
- if(strictRouting) {
503
- if(req.endsWithSlash && req._opPath[req._opPath.length - 1] !== '/') {
504
- req._opPath += '/';
505
- }
506
- }
507
- req.url = req._opPath + req.urlQuery;
508
- req.path = req._opPath;
509
- if(req._opPath === '') {
510
- req.url = '/';
511
- req.path = '/';
512
- }
513
- if(!strictRouting && req.endsWithSlash && req._originalPath !== '/' && req._opPath[req._opPath.length - 1] === '/') {
514
- req._opPath = req._opPath.slice(0, -1);
515
- }
516
- if(req.app.parent && route.callback.constructor.name === 'Application') {
517
- req.app = req.app.parent;
518
- }
519
- }
520
- req.routeCount++;
521
- return resolve(this._routeRequest(req, res, routeIndex + 1, routes, skipCheck, skipUntil));
522
- } else {
523
- req._error = thingamabob;
524
- req._errorKey = route.routeKey;
525
- }
526
- }
527
- const callback = route.callbacks[callbackindex++];
528
- if(!callback) {
529
- return next('route');
530
- }
531
- if(callback instanceof Router) {
532
- if(callback.constructor.name === 'Application') {
533
- req.app = callback;
534
- }
535
- if(callback.settings.mergeParams) {
536
- req._paramStack.push(req.params);
537
- }
538
- if(callback.settings['strict routing'] && req.endsWithSlash && req._opPath[req._opPath.length - 1] !== '/') {
539
- req._opPath += '/';
540
- }
541
- const routed = await callback._routeRequest(req, res, 0);
542
- if (req._error) {
543
- req._errorKey = route.routeKey;
544
- }
545
- if(routed) return resolve(true);
546
- else if(req._isOptions && req._matchedMethods.size) {
547
- // OPTIONS routing is different, it stops in the router if matched
548
- return resolve(false);
549
- }
550
- next();
551
- } else {
552
- // handle errors and error handlers
553
- if(req._error || callback.length === 4) {
554
- if(req._error && callback.length === 4 && route.routeKey >= req._errorKey) {
555
- return this._handleError(req._error, callback, req, res);
556
- } else {
557
- return next();
558
- }
559
- }
560
-
561
- try {
562
- // handling OPTIONS method
563
- if(req._isOptions && !route.all && route.method !== 'OPTIONS') {
564
- req._matchedMethods.add(route.method);
565
- if(route.gettable) {
566
- req._matchedMethods.add('HEAD');
567
- }
568
- return next();
569
- }
570
-
571
- // skipping routes we already went through via optimized path
572
- if(!skipCheck && skipUntil && skipUntil.routeKey >= route.routeKey) {
573
- return next();
574
- }
575
- const out = callback(req, res, next);
576
- if(out instanceof Promise) {
577
- out.catch(err => {
578
- if(this.get("catch async errors")) {
579
- req._error = err;
580
- req._errorKey = route.routeKey;
581
- return next();
582
- } else {
583
- throw err;
584
- }
585
- });
586
- }
587
- } catch(err) {
588
- req._error = err;
589
- req._errorKey = route.routeKey;
590
- return next();
591
- }
592
- }
593
- }
594
- req.next = next;
595
- if(continueRoute === 'route') {
596
- next('route');
597
- } else if(continueRoute) {
598
- next();
599
- } else {
600
- resolve(true);
601
- }
602
- });
603
- }
604
-
605
- use(path, ...callbacks) {
606
- if(typeof path === 'function' || path instanceof Router || (Array.isArray(path) && path.every(p => typeof p === 'function' || p instanceof Router))) {
607
- callbacks.unshift(path);
608
- path = '';
609
- }
610
- if(path === '/') {
611
- path = '';
612
- }
613
- callbacks = callbacks.flat();
614
-
615
- for(let callback of callbacks) {
616
- if(callback instanceof Router) {
617
- callback.mountpath = path;
618
- callback.parent = this;
619
- callback.emit('mount', this);
620
- }
621
- }
622
- this.createRoute('USE', path, this, ...callbacks);
623
- return this;
624
- }
625
-
626
- route(path) {
627
- let fns = new NullObject();
628
- for(let method of methods) {
629
- fns[method] = (...callbacks) => {
630
- return this.createRoute(method.toUpperCase(), path, fns, ...callbacks);
631
- };
632
- }
633
- fns.get = (...callbacks) => {
634
- return this.createRoute('GET', path, fns, ...callbacks);
635
- };
636
- return fns;
637
- }
638
-
639
- _sendErrorPage(request, response, err, checkEnv = false) {
640
- if(checkEnv && this.get('env') === 'production') {
641
- err = response.statusCode >= 400 ? (statuses.message[response.statusCode] ?? 'Internal Server Error') : 'Internal Server Error';
642
- }
643
- request.noEtag = true;
644
- response.setHeader('Content-Type', 'text/html; charset=utf-8');
645
- response.setHeader('X-Content-Type-Options', 'nosniff');
646
- response.setHeader('Content-Security-Policy', "default-src 'none'");
647
- response.send(`<!DOCTYPE html>\n` +
648
- `<html lang="en">\n` +
649
- `<head>\n` +
650
- `<meta charset="utf-8">\n` +
651
- `<title>Error</title>\n` +
652
- `</head>\n` +
653
- `<body>\n` +
654
- `<pre>${err?.stack ?? err}</pre>\n` +
655
- `</body>\n` +
656
- `</html>\n`);
657
- }
658
- }
1
+ /*
2
+ Copyright 2024 dimden.dev
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ const { patternToRegex, needsConversionToRegex, deprecated, findIndexStartingFrom, canBeOptimized, NullObject, EMPTY_REGEX } = require("./utils.js");
18
+ const Response = require("./response.js");
19
+ const Request = require("./request.js");
20
+ const { EventEmitter } = require("tseep");
21
+ const compileDeclarative = require("./declarative.js");
22
+ const statuses = require("statuses");
23
+
24
+ let resCodes = {}, resDecMethods = ['set', 'setHeader', 'header', 'send', 'end', 'append', 'status'];
25
+ for(let method of resDecMethods) {
26
+ resCodes[method] = Response.prototype[method].toString();
27
+ }
28
+
29
+ let routeKey = 0;
30
+
31
+ const methods = [
32
+ 'all',
33
+ 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace', 'connect',
34
+ 'checkout', 'copy', 'lock', 'mkcol', 'move', 'purge', 'propfind', 'proppatch',
35
+ 'search', 'subscribe', 'unsubscribe', 'report', 'mkactivity', 'mkcalendar',
36
+ 'checkout', 'merge', 'm-search', 'notify', 'subscribe', 'unsubscribe', 'search'
37
+ ];
38
+ const supportedUwsMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE'];
39
+
40
+ const regExParam = /:(\w+)/g;
41
+
42
+ module.exports = class Router extends EventEmitter {
43
+ constructor(settings = {}) {
44
+ super();
45
+
46
+ this._paramCallbacks = new Map();
47
+ this._mountpathCache = new Map();
48
+ this._routes = [];
49
+ this.mountpath = '/';
50
+ this.settings = settings;
51
+ this._request = Request;
52
+ this._response = Response;
53
+ this.request = this._request.prototype;
54
+ this.response = this._response.prototype;
55
+
56
+ if(typeof settings.caseSensitive !== 'undefined') {
57
+ this.settings['case sensitive routing'] = settings.caseSensitive;
58
+ delete this.settings.caseSensitive;
59
+ }
60
+ if(typeof settings.strict !== 'undefined') {
61
+ this.settings['strict routing'] = settings.strict;
62
+ delete this.settings.strict;
63
+ }
64
+
65
+ if(typeof this.settings['case sensitive routing'] === 'undefined') {
66
+ this.settings['case sensitive routing'] = true;
67
+ }
68
+
69
+ for(let method of methods) {
70
+ this[method] = (path, ...callbacks) => {
71
+ return this.createRoute(method, path, this, ...callbacks);
72
+ };
73
+ };
74
+ }
75
+
76
+ get(path, ...callbacks) {
77
+ if(typeof path === 'string' && callbacks.length === 0) {
78
+ const key = path;
79
+ const res = this.settings[key];
80
+ if(typeof res === 'undefined' && this.parent) {
81
+ return this.parent.get(key);
82
+ } else {
83
+ return res;
84
+ }
85
+ }
86
+ return this.createRoute('GET', path, this, ...callbacks);
87
+ }
88
+
89
+ del(path, ...callbacks) {
90
+ deprecated('app.del', 'app.delete');
91
+ return this.createRoute('DELETE', path, this, ...callbacks);
92
+ }
93
+
94
+ getFullMountpath(req) {
95
+ let fullStack = req._stack.join("");
96
+ if(!fullStack){
97
+ return EMPTY_REGEX;
98
+ }
99
+ let fullMountpath = this._mountpathCache.get(fullStack);
100
+ if(!fullMountpath) {
101
+ fullMountpath = patternToRegex(fullStack, true);
102
+ this._mountpathCache.set(fullStack, fullMountpath);
103
+ }
104
+ return fullMountpath;
105
+ }
106
+
107
+ _pathMatches(route, req) {
108
+ let path = req._opPath;
109
+ let pattern = route.pattern;
110
+
111
+ if(req.endsWithSlash && path.endsWith('/') && !this.get('strict routing')) {
112
+ path = path.slice(0, -1);
113
+ }
114
+
115
+ if (typeof pattern === 'string') {
116
+ if(pattern === '/*') {
117
+ return true;
118
+ }
119
+ if(path === '') {
120
+ path = '/';
121
+ }
122
+ if(!this.get('case sensitive routing')) {
123
+ path = path.toLowerCase();
124
+ pattern = pattern.toLowerCase();
125
+ }
126
+ return pattern === path;
127
+ }
128
+ if (pattern === EMPTY_REGEX){
129
+ return true;
130
+ }
131
+ return pattern.test(path);
132
+ }
133
+
134
+ createRoute(method, path, parent = this, ...callbacks) {
135
+ method = method.toUpperCase();
136
+ callbacks = callbacks.flat();
137
+ const paths = Array.isArray(path) ? path : [path];
138
+ const routes = [];
139
+ for(let path of paths) {
140
+ if(!this.get('strict routing') && typeof path === 'string' && path.endsWith('/') && path !== '/') {
141
+ path = path.slice(0, -1);
142
+ }
143
+ if(path === '*') {
144
+ path = '/*';
145
+ }
146
+ const route = {
147
+ method: method === 'USE' ? 'ALL' : method,
148
+ path,
149
+ pattern: method === 'USE' || needsConversionToRegex(path) ? patternToRegex(path, method === 'USE') : path,
150
+ callbacks,
151
+ routeKey: routeKey++,
152
+ use: method === 'USE',
153
+ all: method === 'ALL' || method === 'USE',
154
+ gettable: method === 'GET' || method === 'HEAD',
155
+ };
156
+ if(typeof route.path === 'string' && (route.path.includes(':') || route.path.includes('*') || (route.path.includes('(') && route.path.includes(')'))) && route.pattern instanceof RegExp) {
157
+ route.complex = true;
158
+ }
159
+ routes.push(route);
160
+ // normal routes optimization
161
+ if(canBeOptimized(route.path) && route.pattern !== '/*' && !this.parent && this.get('case sensitive routing') && this.uwsApp) {
162
+ if(supportedUwsMethods.includes(method)) {
163
+ const optimizedPath = this._optimizeRoute(route, this._routes);
164
+ if(optimizedPath) {
165
+ this._registerUwsRoute(route, optimizedPath);
166
+ }
167
+ }
168
+ }
169
+ // router optimization
170
+ if(
171
+ route.use && !needsConversionToRegex(path) && path !== '/*' && // must be predictable path
172
+ this.get('case sensitive routing') && // uWS only supports case sensitive routing
173
+ callbacks.filter(c => c instanceof Router).length === 1 && // only 1 router can be optimized per route
174
+ callbacks[callbacks.length - 1] instanceof Router // the router must be the last callback
175
+ ) {
176
+ let callbacksBeforeRouter = [];
177
+ for(let callback of callbacks) {
178
+ if(callback instanceof Router) {
179
+ // get optimized path to router
180
+ let optimizedPathToRouter = this._optimizeRoute(route, this._routes);
181
+ if(!optimizedPathToRouter) {
182
+ break;
183
+ }
184
+ optimizedPathToRouter = optimizedPathToRouter.slice(0, -1); // remove last element, which is the router itself
185
+ if(optimizedPathToRouter) {
186
+ // wait for routes in router to be registered
187
+ const t = setTimeout(() => {
188
+ if(!this.listenCalled) {
189
+ return; // can only optimize router whos parent is listening
190
+ }
191
+ for(let cbroute of callback._routes) {
192
+ if(!needsConversionToRegex(cbroute.path) && cbroute.path !== '/*' && supportedUwsMethods.includes(cbroute.method)) {
193
+ let optimizedRouterPath = this._optimizeRoute(cbroute, callback._routes);
194
+ if(optimizedRouterPath) {
195
+ optimizedRouterPath = optimizedRouterPath.slice(0, -1);
196
+ const optimizedPath = [...optimizedPathToRouter, {
197
+ // fake route to update req._opPath and req.url
198
+ ...route,
199
+ callbacks: [
200
+ (req, res, next) => {
201
+ next('skipPop');
202
+ }
203
+ ]
204
+ }, ...optimizedRouterPath];
205
+ this._registerUwsRoute({
206
+ ...cbroute,
207
+ path: route.path + cbroute.path,
208
+ pattern: route.path + cbroute.path,
209
+ optimizedRouter: true
210
+ }, optimizedPath);
211
+ }
212
+ }
213
+ }
214
+ }, 100);
215
+ t.unref();
216
+ }
217
+ // only 1 router can be optimized per route
218
+ break;
219
+ } else {
220
+ callbacksBeforeRouter.push(route);
221
+ }
222
+ }
223
+ }
224
+ }
225
+ this._routes.push(...routes);
226
+
227
+ return parent;
228
+ }
229
+
230
+ // if route is a simple string, its possible to pre-calculate its path
231
+ // and then create a native uWS route for it, which is much faster
232
+ _optimizeRoute(route, routes) {
233
+ const optimizedPath = [];
234
+
235
+ for(let i = 0; i < routes.length; i++) {
236
+ const r = routes[i];
237
+ if(r.routeKey > route.routeKey) {
238
+ break;
239
+ }
240
+ // if the methods are not the same, and its not an all method, skip it
241
+ if(!r.all && r.method !== route.method) {
242
+ // check if the methods are compatible (GET and HEAD)
243
+ if(!(r.method === 'HEAD' && route.method === 'GET')) {
244
+ continue;
245
+ }
246
+ }
247
+
248
+ // check if the paths match
249
+ if(
250
+ (r.pattern instanceof RegExp && r.pattern.test(route.path)) ||
251
+ (typeof r.pattern === 'string' && (r.pattern === route.path || r.pattern === '/*'))
252
+ ) {
253
+ if(r.callbacks.some(c => c instanceof Router)) {
254
+ return false; // cant optimize nested routers with matches
255
+ }
256
+ optimizedPath.push(r);
257
+ }
258
+ }
259
+ optimizedPath.push(route);
260
+
261
+ return optimizedPath;
262
+ }
263
+
264
+ handleRequest(res, req) {
265
+ const request = new this._request(req, res, this);
266
+ const response = new this._response(res, request, this);
267
+ request.res = response;
268
+ response.req = request;
269
+ res.onAborted(() => {
270
+ const err = new Error('Connection closed');
271
+ err.code = 'ECONNRESET';
272
+ response.aborted = true;
273
+ response.finished = true;
274
+ response.socket?.emit('error', err);
275
+ });
276
+
277
+ return { request, response };
278
+ }
279
+
280
+ _registerUwsRoute(route, optimizedPath) {
281
+ let method = route.method.toLowerCase();
282
+ if(method === 'all') {
283
+ method = 'any';
284
+ } else if(method === 'delete') {
285
+ method = 'del';
286
+ }
287
+ if(!route.optimizedRouter && route.path.includes(":")) {
288
+ route.optimizedParams = route.path.match(regExParam).map(p => p.slice(1));
289
+ }
290
+ let fn = async (res, req) => {
291
+ const { request, response } = this.handleRequest(res, req);
292
+ if(route.optimizedParams) {
293
+ request.optimizedParams = new NullObject();
294
+ for(let i = 0; i < route.optimizedParams.length; i++) {
295
+ request.optimizedParams[route.optimizedParams[i]] = req.getParameter(i);
296
+ }
297
+ }
298
+ const matchedRoute = await this._routeRequest(request, response, 0, optimizedPath, true, route);
299
+ if(!matchedRoute && !response.headersSent && !response.aborted) {
300
+ if(request._error) {
301
+ return this._handleError(request._error, null, request, response);
302
+ }
303
+ if(request._isOptions && request._matchedMethods.size > 0) {
304
+ const allowedMethods = Array.from(request._matchedMethods).join(',');
305
+ response.setHeader('Allow', allowedMethods);
306
+ response.send(allowedMethods);
307
+ return;
308
+ }
309
+ response.status(404);
310
+ request.noEtag = true;
311
+ this._sendErrorPage(request, response, `Cannot ${request.method} ${request._originalPath}`, false);
312
+ }
313
+ };
314
+ route.optimizedPath = optimizedPath;
315
+
316
+ let replacedPath = route.path;
317
+ const realFn = fn;
318
+
319
+ // check if route is declarative
320
+ if(
321
+ optimizedPath.length === 1 && // must not have middlewares
322
+ route.callbacks.length === 1 && // must not have multiple callbacks
323
+ typeof route.callbacks[0] === 'function' && // must be a function
324
+ this._paramCallbacks.size === 0 && // app.param() is not supported
325
+ !resDecMethods.some(method => resCodes[method] !== this.response[method].toString()) && // must not have injected methods
326
+ this.get('declarative responses') // must have declarative responses enabled
327
+ ) {
328
+ const decRes = compileDeclarative(route.callbacks[0], this);
329
+ if(decRes) {
330
+ fn = decRes;
331
+ }
332
+ } else {
333
+ replacedPath = route.path.replace(regExParam, ':x');
334
+ }
335
+
336
+ this.uwsApp[method](replacedPath, fn);
337
+ if(!this.get('strict routing') && route.path[route.path.length - 1] !== '/') {
338
+ this.uwsApp[method](replacedPath + '/', fn);
339
+ if(method === 'get') {
340
+ this.uwsApp.head(replacedPath + '/', realFn);
341
+ }
342
+ }
343
+ if(method === 'get') {
344
+ this.uwsApp.head(replacedPath, realFn);
345
+ }
346
+ }
347
+
348
+ _handleError(err, handler, request, response) {
349
+ if(handler) {
350
+ return handler(err, request, response, () => {
351
+ delete request._error;
352
+ delete request._errorKey;
353
+ return request.next();
354
+ });
355
+ }
356
+ console.error(err);
357
+ if(response.statusCode === 200) {
358
+ response.statusCode = 500;
359
+ }
360
+ this._sendErrorPage(request, response, err, true);
361
+ }
362
+
363
+ _extractParams(pattern, path) {
364
+ if(path.endsWith('/')) {
365
+ path = path.slice(0, -1);
366
+ }
367
+ let match = pattern.exec(path);
368
+ if( match?.groups ){
369
+ return match.groups;
370
+ }
371
+ const obj = new NullObject();
372
+ for(let i = 1; i < match.length; i++) {
373
+ obj[i - 1] = match[i];
374
+ }
375
+ return obj;
376
+ }
377
+
378
+ _preprocessRequest(req, res, route) {
379
+ req.route = route;
380
+ if(route.optimizedParams) {
381
+ req.params = {...req.optimizedParams};
382
+ } else if(route.complex) {
383
+ let path = req._originalPath;
384
+ if(req._stack.length > 0) {
385
+ path = path.replace(this.getFullMountpath(req), '');
386
+ }
387
+ req.params = {...this._extractParams(route.pattern, path)};
388
+ if(req._paramStack.length > 0) {
389
+ for(let params of req._paramStack) {
390
+ req.params = {...params, ...req.params};
391
+ }
392
+ }
393
+ } else {
394
+ req.params = {};
395
+ if(req._paramStack.length > 0) {
396
+ for(let params of req._paramStack) {
397
+ req.params = {...params, ...req.params};
398
+ }
399
+ }
400
+ }
401
+
402
+ if(this._paramCallbacks.size > 0) {
403
+ return new Promise(async resolve => {
404
+ for(let param in req.params) {
405
+ const pcs = this._paramCallbacks.get(param);
406
+ if(pcs && !req._gotParams.has(param)) {
407
+ req._gotParams.add(param);
408
+ for(let i = 0, len = pcs.length; i < len; i++) {
409
+ const fn = pcs[i];
410
+ await new Promise(resolveRoute => {
411
+ const next = (thingamabob) => {
412
+ if(thingamabob) {
413
+ if(thingamabob === 'route') {
414
+ return resolve('route');
415
+ } else {
416
+ req._error = thingamabob;
417
+ req._errorKey = route.routeKey;
418
+ }
419
+ }
420
+ return resolveRoute();
421
+ };
422
+ req.next = next;
423
+ fn(req, res, next, req.params[param], param);
424
+ });
425
+ }
426
+ }
427
+ }
428
+
429
+ resolve(true)
430
+ });
431
+ }
432
+ return true;
433
+ }
434
+
435
+ param(name, fn) {
436
+ if(typeof name === 'function') {
437
+ deprecated('app.param(callback)', 'app.param(name, callback)', true);
438
+ this._paramFunction = name;
439
+ } else {
440
+ if(this._paramFunction) {
441
+ if(!this._paramCallbacks.has(name)) {
442
+ this._paramCallbacks.set(name, []);
443
+ }
444
+ this._paramCallbacks.get(name).push(this._paramFunction(name, fn));
445
+ } else {
446
+ let names = Array.isArray(name) ? name : [name];
447
+ for(let name of names) {
448
+ if(!this._paramCallbacks.has(name)) {
449
+ this._paramCallbacks.set(name, []);
450
+ }
451
+ this._paramCallbacks.get(name).push(fn);
452
+ }
453
+ }
454
+ }
455
+ }
456
+
457
+ async _routeRequest(req, res, startIndex = 0, routes = this._routes, skipCheck = false, skipUntil) {
458
+ let routeIndex = skipCheck ? startIndex : findIndexStartingFrom(routes, r => (r.all || r.method === req.method || req._isOptions || (r.gettable && req._isHead)) && this._pathMatches(r, req), startIndex);
459
+ const route = routes[routeIndex];
460
+ if(!route) {
461
+ if(!skipCheck) {
462
+ // on normal unoptimized routes, if theres no match then there is no route
463
+ return false;
464
+ }
465
+ // on optimized routes, there can be more routes, so we have to use unoptimized routing and skip until we find route we stopped at
466
+ return this._routeRequest(req, res, 0, this._routes, false, skipUntil);
467
+ }
468
+ let callbackindex = 0;
469
+
470
+ // avoid calling _preprocessRequest as async function as its slower
471
+ // but it seems like calling it as async has unintended, but useful consequence of resetting max call stack size
472
+ // so call it as async when the request has been through every 300 routes to reset it
473
+ const continueRoute = this._paramCallbacks.size === 0 && req.routeCount % 300 !== 0 ?
474
+ this._preprocessRequest(req, res, route) : await this._preprocessRequest(req, res, route);
475
+
476
+ const strictRouting = this.get('strict routing');
477
+ if(route.use) {
478
+ req._stack.push(route.path);
479
+ const fullMountpath = this.getFullMountpath(req);
480
+ req._opPath = fullMountpath !== EMPTY_REGEX ? req._originalPath.replace(fullMountpath, '') : req._originalPath;
481
+ if(req.endsWithSlash && req._opPath[req._opPath.length - 1] !== '/') {
482
+ if(strictRouting) {
483
+ req._opPath += '/';
484
+ } else {
485
+ req._opPath = req._opPath.slice(0, -1);
486
+ }
487
+ }
488
+ req.url = req._opPath + req.urlQuery;
489
+ req.path = req._opPath;
490
+ if(req._opPath === '') {
491
+ req.url = '/';
492
+ req.path = '/';
493
+ }
494
+ }
495
+ return new Promise((resolve) => {
496
+ const next = async (thingamabob) => {
497
+ if(thingamabob) {
498
+ if(thingamabob === 'route' || thingamabob === 'skipPop') {
499
+ if(route.use && thingamabob !== 'skipPop') {
500
+ req._stack.pop();
501
+
502
+ req._opPath = req._stack.length > 0 ? req._originalPath.replace(this.getFullMountpath(req), '') : req._originalPath;
503
+ if(strictRouting) {
504
+ if(req.endsWithSlash && req._opPath[req._opPath.length - 1] !== '/') {
505
+ req._opPath += '/';
506
+ }
507
+ }
508
+ req.url = req._opPath + req.urlQuery;
509
+ req.path = req._opPath;
510
+ if(req._opPath === '') {
511
+ req.url = '/';
512
+ req.path = '/';
513
+ }
514
+ if(!strictRouting && req.endsWithSlash && req._originalPath !== '/' && req._opPath[req._opPath.length - 1] === '/') {
515
+ req._opPath = req._opPath.slice(0, -1);
516
+ }
517
+ if(req.app.parent && route.callbacks[0]?.constructor.name === 'Application') {
518
+ req.app = req.app.parent;
519
+ }
520
+ }
521
+ req.routeCount++;
522
+ return resolve(this._routeRequest(req, res, routeIndex + 1, routes, skipCheck, skipUntil));
523
+ } else {
524
+ req._error = thingamabob;
525
+ req._errorKey = route.routeKey;
526
+ }
527
+ }
528
+ const callback = route.callbacks[callbackindex++];
529
+ if(!callback) {
530
+ return next('route');
531
+ }
532
+ if(callback instanceof Router) {
533
+ if(callback.constructor.name === 'Application') {
534
+ req.app = callback;
535
+ }
536
+ if(callback.settings.mergeParams) {
537
+ req._paramStack.push(req.params);
538
+ }
539
+ if(callback.settings['strict routing'] && req.endsWithSlash && req._opPath[req._opPath.length - 1] !== '/') {
540
+ req._opPath += '/';
541
+ }
542
+ const routed = await callback._routeRequest(req, res, 0);
543
+ if (req._error) {
544
+ req._errorKey = route.routeKey;
545
+ }
546
+ if(routed) return resolve(true);
547
+ else if(req._isOptions && req._matchedMethods.size) {
548
+ // OPTIONS routing is different, it stops in the router if matched
549
+ return resolve(false);
550
+ }
551
+ next();
552
+ } else {
553
+ // handle errors and error handlers
554
+ if(req._error || callback.length === 4) {
555
+ if(req._error && callback.length === 4 && route.routeKey >= req._errorKey) {
556
+ return this._handleError(req._error, callback, req, res);
557
+ } else {
558
+ return next();
559
+ }
560
+ }
561
+
562
+ try {
563
+ // handling OPTIONS method
564
+ if(req._isOptions && !route.all && route.method !== 'OPTIONS') {
565
+ req._matchedMethods.add(route.method);
566
+ if(route.gettable) {
567
+ req._matchedMethods.add('HEAD');
568
+ }
569
+ return next();
570
+ }
571
+
572
+ // skipping routes we already went through via optimized path
573
+ if(!skipCheck && skipUntil && skipUntil.routeKey >= route.routeKey) {
574
+ return next();
575
+ }
576
+ const out = callback(req, res, next);
577
+ if(out instanceof Promise) {
578
+ out.catch(err => {
579
+ if(this.get("catch async errors")) {
580
+ req._error = err;
581
+ req._errorKey = route.routeKey;
582
+ return next();
583
+ } else {
584
+ throw err;
585
+ }
586
+ });
587
+ }
588
+ } catch(err) {
589
+ req._error = err;
590
+ req._errorKey = route.routeKey;
591
+ return next();
592
+ }
593
+ }
594
+ }
595
+ req.next = next;
596
+ if(continueRoute === 'route') {
597
+ next('route');
598
+ } else if(continueRoute) {
599
+ next();
600
+ } else {
601
+ resolve(true);
602
+ }
603
+ });
604
+ }
605
+
606
+ use(path, ...callbacks) {
607
+ if(typeof path === 'function' || path instanceof Router || (Array.isArray(path) && path.every(p => typeof p === 'function' || p instanceof Router))) {
608
+ callbacks.unshift(path);
609
+ path = '';
610
+ }
611
+ if(path === '/') {
612
+ path = '';
613
+ }
614
+ callbacks = callbacks.flat();
615
+
616
+ for(let callback of callbacks) {
617
+ if(callback instanceof Router) {
618
+ callback.mountpath = path;
619
+ callback.parent = this;
620
+ callback.emit('mount', this);
621
+ }
622
+ }
623
+ this.createRoute('USE', path, this, ...callbacks);
624
+ return this;
625
+ }
626
+
627
+ route(path) {
628
+ let fns = new NullObject();
629
+ for(let method of methods) {
630
+ fns[method] = (...callbacks) => {
631
+ return this.createRoute(method, path, fns, ...callbacks);
632
+ };
633
+ }
634
+ fns.get = (...callbacks) => {
635
+ return this.createRoute('GET', path, fns, ...callbacks);
636
+ };
637
+ return fns;
638
+ }
639
+
640
+ _sendErrorPage(request, response, err, checkEnv = false) {
641
+ if(checkEnv && this.get('env') === 'production') {
642
+ err = response.statusCode >= 400 ? (statuses.message[response.statusCode] ?? 'Internal Server Error') : 'Internal Server Error';
643
+ }
644
+ request.noEtag = true;
645
+ response.setHeader('Content-Type', 'text/html; charset=utf-8');
646
+ response.setHeader('X-Content-Type-Options', 'nosniff');
647
+ response.setHeader('Content-Security-Policy', "default-src 'none'");
648
+ response.send(`<!DOCTYPE html>\n` +
649
+ `<html lang="en">\n` +
650
+ `<head>\n` +
651
+ `<meta charset="utf-8">\n` +
652
+ `<title>Error</title>\n` +
653
+ `</head>\n` +
654
+ `<body>\n` +
655
+ `<pre>${err?.stack ?? err}</pre>\n` +
656
+ `</body>\n` +
657
+ `</html>\n`);
658
+ }
659
+ }