@deepkit/http 1.0.18 → 1.1.0
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/CHANGELOG.md +45 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/src/decorator.js +11 -7
- package/dist/cjs/src/decorator.js.map +1 -1
- package/dist/cjs/src/filter.js +1 -1
- package/dist/cjs/src/filter.js.map +1 -1
- package/dist/cjs/src/http.d.ts +5 -2
- package/dist/cjs/src/http.js +93 -67
- package/dist/cjs/src/http.js.map +1 -1
- package/dist/cjs/src/kernel.d.ts +2 -2
- package/dist/cjs/src/kernel.js +7 -14
- package/dist/cjs/src/kernel.js.map +1 -1
- package/dist/cjs/src/logger.js +0 -7
- package/dist/cjs/src/logger.js.map +1 -1
- package/dist/cjs/src/middleware.d.ts +2 -2
- package/dist/cjs/src/middleware.js +2 -2
- package/dist/cjs/src/mime.d.ts +19 -0
- package/dist/cjs/src/mime.js +147 -0
- package/dist/cjs/src/mime.js.map +1 -0
- package/dist/cjs/src/model.d.ts +7 -0
- package/dist/cjs/src/model.js +16 -7
- package/dist/cjs/src/model.js.map +1 -1
- package/dist/cjs/src/module.js +9 -5
- package/dist/cjs/src/module.js.map +1 -1
- package/dist/cjs/src/query-string.d.ts +6 -0
- package/dist/cjs/src/query-string.js +229 -0
- package/dist/cjs/src/query-string.js.map +1 -0
- package/dist/cjs/src/request-parser.js +12 -10
- package/dist/cjs/src/request-parser.js.map +1 -1
- package/dist/cjs/src/router.js +14 -15
- package/dist/cjs/src/router.js.map +1 -1
- package/dist/cjs/src/static-serving.d.ts +6 -0
- package/dist/cjs/src/static-serving.js +232 -28
- package/dist/cjs/src/static-serving.js.map +1 -1
- package/dist/cjs/src/utils.js +1 -1
- package/dist/cjs/src/utils.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/src/decorator.js +11 -7
- package/dist/esm/src/decorator.js.map +1 -1
- package/dist/esm/src/filter.js +1 -1
- package/dist/esm/src/filter.js.map +1 -1
- package/dist/esm/src/http.d.ts +5 -2
- package/dist/esm/src/http.js +58 -66
- package/dist/esm/src/http.js.map +1 -1
- package/dist/esm/src/kernel.d.ts +2 -2
- package/dist/esm/src/kernel.js +8 -15
- package/dist/esm/src/kernel.js.map +1 -1
- package/dist/esm/src/logger.js +0 -7
- package/dist/esm/src/logger.js.map +1 -1
- package/dist/esm/src/middleware.d.ts +2 -2
- package/dist/esm/src/middleware.js +2 -2
- package/dist/esm/src/mime.d.ts +19 -0
- package/dist/esm/src/mime.js +142 -0
- package/dist/esm/src/mime.js.map +1 -0
- package/dist/esm/src/model.d.ts +7 -0
- package/dist/esm/src/model.js +16 -7
- package/dist/esm/src/model.js.map +1 -1
- package/dist/esm/src/module.js +9 -5
- package/dist/esm/src/module.js.map +1 -1
- package/dist/esm/src/query-string.d.ts +6 -0
- package/dist/esm/src/query-string.js +226 -0
- package/dist/esm/src/query-string.js.map +1 -0
- package/dist/esm/src/request-parser.js +12 -10
- package/dist/esm/src/request-parser.js.map +1 -1
- package/dist/esm/src/router.js +14 -12
- package/dist/esm/src/router.js.map +1 -1
- package/dist/esm/src/static-serving.d.ts +6 -0
- package/dist/esm/src/static-serving.js +234 -27
- package/dist/esm/src/static-serving.js.map +1 -1
- package/dist/esm/src/utils.js +1 -1
- package/dist/esm/src/utils.js.map +1 -1
- package/index.ts +2 -0
- package/package.json +22 -25
- package/src/http.ts +43 -10
- package/src/kernel.ts +8 -21
- package/src/middleware.ts +2 -2
- package/src/mime.ts +141 -0
- package/src/model.ts +10 -2
- package/src/module.ts +4 -2
- package/src/query-string.ts +228 -0
- package/src/request-parser.ts +6 -4
- package/src/router.ts +3 -3
- package/src/static-serving.ts +221 -8
- package/tsconfig.json +3 -17
- package/tsconfig.spec.json +4 -1
- package/LICENSE +0 -21
- package/dist/cjs/tsconfig.tsbuildinfo +0 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +0 -1
package/src/http.ts
CHANGED
|
@@ -27,9 +27,10 @@ export function isElementStruct(v: any): v is ElementStruct {
|
|
|
27
27
|
|
|
28
28
|
let templateRender: typeof render;
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// @deepkit/template is optional, so it's only loaded when a controller returns a JSX element.
|
|
31
|
+
async function getTemplateRender(): Promise<typeof render> {
|
|
31
32
|
if (!templateRender) {
|
|
32
|
-
const template =
|
|
33
|
+
const template = await import('@deepkit/template');
|
|
33
34
|
templateRender = template.render;
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -110,6 +111,9 @@ export class HttpInternalServerError extends createHttpError(500, 'Internal serv
|
|
|
110
111
|
export class HttpNotImplementedError extends createHttpError(501, 'Not implemented') {
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
export class HttpGatewayTimeoutError extends createHttpError(504, 'Gateway timeout') {
|
|
115
|
+
}
|
|
116
|
+
|
|
113
117
|
export class HttpWorkflowEvent extends BaseEvent {
|
|
114
118
|
public nextState?: any;
|
|
115
119
|
public nextStateEvent?: any;
|
|
@@ -318,7 +322,7 @@ export const httpWorkflow = createWorkflow('http', {
|
|
|
318
322
|
}, {
|
|
319
323
|
start: 'request',
|
|
320
324
|
request: 'route',
|
|
321
|
-
route: ['auth', 'routeNotFound'],
|
|
325
|
+
route: ['auth', 'routeNotFound', 'response'],
|
|
322
326
|
auth: ['resolveParameters', 'accessDenied'],
|
|
323
327
|
resolveParameters: ['controller', 'parametersFailed'],
|
|
324
328
|
accessDenied: 'response',
|
|
@@ -541,9 +545,9 @@ export class HttpListener {
|
|
|
541
545
|
protected logger: LoggerInterface,
|
|
542
546
|
protected resultFormatter: HttpResultFormatter,
|
|
543
547
|
protected injector: Injector,
|
|
544
|
-
protected stopwatch
|
|
548
|
+
protected stopwatch: Stopwatch,
|
|
545
549
|
) {
|
|
546
|
-
this.setRouteConfig = injector.createSetter(RouteConfig,
|
|
550
|
+
this.setRouteConfig = injector.createSetter(RouteConfig, 'http');
|
|
547
551
|
}
|
|
548
552
|
|
|
549
553
|
@eventDispatcher.listen(httpWorkflow.onRequest, 100)
|
|
@@ -559,6 +563,7 @@ export class HttpListener {
|
|
|
559
563
|
if (event.sent) return;
|
|
560
564
|
if (event.hasNext()) return;
|
|
561
565
|
const logger = this.logger;
|
|
566
|
+
let inMiddleware = false;
|
|
562
567
|
|
|
563
568
|
try {
|
|
564
569
|
const resolved = this.router.resolveRequest(event.request);
|
|
@@ -570,6 +575,7 @@ export class HttpListener {
|
|
|
570
575
|
if (resolved.middlewares) {
|
|
571
576
|
const middlewares = resolved.middlewares(event.injectorContext);
|
|
572
577
|
if (middlewares.length) {
|
|
578
|
+
inMiddleware = true;
|
|
573
579
|
await asyncOperation(async (resolve, reject) => {
|
|
574
580
|
let lastTimer: any = undefined;
|
|
575
581
|
|
|
@@ -581,6 +587,7 @@ export class HttpListener {
|
|
|
581
587
|
|
|
582
588
|
event.response.once('finish', finish);
|
|
583
589
|
let i = -1;
|
|
590
|
+
let timedOut = false;
|
|
584
591
|
|
|
585
592
|
async function next() {
|
|
586
593
|
i++;
|
|
@@ -594,14 +601,18 @@ export class HttpListener {
|
|
|
594
601
|
const timeout = middlewares[i].timeout;
|
|
595
602
|
if (timeout !== undefined && timeout > 0) {
|
|
596
603
|
lastTimer = setTimeout(() => {
|
|
604
|
+
//a middleware that neither calls next() nor ends the response in time fails the request
|
|
605
|
+
timedOut = true;
|
|
597
606
|
logger.warn(`Middleware timed out. Increase the timeout or fix the middleware. (${middlewares[i].fn})`);
|
|
598
|
-
|
|
607
|
+
event.response.off('finish', finish);
|
|
608
|
+
reject(new HttpGatewayTimeoutError('Middleware timed out'));
|
|
599
609
|
}, timeout);
|
|
600
610
|
}
|
|
601
611
|
|
|
602
612
|
try {
|
|
603
613
|
await middlewares[i].fn(event.request, event.response, (error?: any) => {
|
|
604
614
|
if (lastTimer) clearTimeout(lastTimer);
|
|
615
|
+
if (timedOut) return;
|
|
605
616
|
if (error) {
|
|
606
617
|
event.response.off('finish', finish);
|
|
607
618
|
reject(error);
|
|
@@ -610,19 +621,42 @@ export class HttpListener {
|
|
|
610
621
|
}
|
|
611
622
|
});
|
|
612
623
|
} catch (error) {
|
|
624
|
+
event.response.off('finish', finish);
|
|
613
625
|
reject(error);
|
|
614
626
|
}
|
|
615
627
|
}
|
|
616
628
|
|
|
617
629
|
await next();
|
|
618
630
|
});
|
|
631
|
+
inMiddleware = false;
|
|
619
632
|
}
|
|
620
633
|
}
|
|
621
634
|
|
|
635
|
+
if (event.sent) {
|
|
636
|
+
//a middleware ended the request, so we skip the controller but still trigger onResponse (e.g. for logging)
|
|
637
|
+
event.next('response', new HttpResponseEvent(event.injectorContext, event.request, event.response, undefined, resolved.routeConfig));
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
622
641
|
this.setRouteConfig(resolved.routeConfig, event.injectorContext.scope);
|
|
623
642
|
event.routeFound(resolved.routeConfig, resolved.parameters);
|
|
624
643
|
}
|
|
625
644
|
} catch (error) {
|
|
645
|
+
if (error instanceof HttpError) {
|
|
646
|
+
if (event.request.throwErrorOnNotFound && error.httpCode === 404) {
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
649
|
+
event.send(new JSONResponse({
|
|
650
|
+
message: error.message
|
|
651
|
+
}, error.httpCode).disableAutoSerializing());
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (inMiddleware) {
|
|
655
|
+
//like a failing controller, an error of a middleware is a server error (#439)
|
|
656
|
+
this.logger.error('Middleware error', error);
|
|
657
|
+
event.send(new HtmlResponse('Internal error', 500));
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
626
660
|
this.logger.error('Could not resolve request', error);
|
|
627
661
|
event.notFound();
|
|
628
662
|
}
|
|
@@ -690,7 +724,7 @@ export class HttpListener {
|
|
|
690
724
|
? getClassName(event.route.action.controller) + '.' + event.route.action.methodName
|
|
691
725
|
: event.route.action.fn.name;
|
|
692
726
|
|
|
693
|
-
const frame = this.stopwatch
|
|
727
|
+
const frame = this.stopwatch.start(stopWatchLabel, FrameCategory.httpController);
|
|
694
728
|
try {
|
|
695
729
|
let result: any;
|
|
696
730
|
if (event.route.action.type === 'controller') {
|
|
@@ -702,7 +736,7 @@ export class HttpListener {
|
|
|
702
736
|
}
|
|
703
737
|
|
|
704
738
|
if (isElementStruct(result)) {
|
|
705
|
-
const html = await getTemplateRender()(event.injectorContext.getRootInjector(), result, this.stopwatch ? this.stopwatch : undefined);
|
|
739
|
+
const html = await (await getTemplateRender())(event.injectorContext.getRootInjector(), result, this.stopwatch ? this.stopwatch : undefined);
|
|
706
740
|
result = new HtmlResponse(html, 200).header('Content-Type', 'text/html; charset=utf-8');
|
|
707
741
|
}
|
|
708
742
|
if (result instanceof stream.Readable) {
|
|
@@ -716,7 +750,6 @@ export class HttpListener {
|
|
|
716
750
|
responseEvent.controllerActionTime = Date.now() - start;
|
|
717
751
|
event.next('response', responseEvent);
|
|
718
752
|
} catch (error: any) {
|
|
719
|
-
if (frame) frame.end();
|
|
720
753
|
if (error instanceof HttpAccessDeniedError) {
|
|
721
754
|
event.next('accessDenied', new HttpAccessDeniedEvent(event.injectorContext, event.request, event.response, event.route, error));
|
|
722
755
|
} else {
|
|
@@ -725,7 +758,7 @@ export class HttpListener {
|
|
|
725
758
|
event.next('controllerError', errorEvent);
|
|
726
759
|
}
|
|
727
760
|
} finally {
|
|
728
|
-
|
|
761
|
+
frame.end();
|
|
729
762
|
}
|
|
730
763
|
}
|
|
731
764
|
|
package/src/kernel.ts
CHANGED
|
@@ -2,14 +2,7 @@ import { InjectorContext, Setter } from '@deepkit/injector';
|
|
|
2
2
|
import { HttpRouter } from './router.js';
|
|
3
3
|
import { EventDispatcher } from '@deepkit/event';
|
|
4
4
|
import { LoggerInterface } from '@deepkit/logger';
|
|
5
|
-
import {
|
|
6
|
-
HttpRequest,
|
|
7
|
-
HttpResponse,
|
|
8
|
-
incomingMessageToHttpRequest,
|
|
9
|
-
MemoryHttpResponse,
|
|
10
|
-
RequestBuilder,
|
|
11
|
-
serverResponseToHttpResponse,
|
|
12
|
-
} from './model.js';
|
|
5
|
+
import { HttpRequest, HttpResponse, incomingMessageToHttpRequest, MemoryHttpResponse, RequestBuilder, serverResponseToHttpResponse } from './model.js';
|
|
13
6
|
import { HttpError, HttpRequestEvent, HttpResultFormatter, httpWorkflow, JSONResponse } from './http.js';
|
|
14
7
|
import { FrameCategory, Stopwatch } from '@deepkit/stopwatch';
|
|
15
8
|
import { unlink } from 'fs';
|
|
@@ -43,7 +36,7 @@ export class HttpKernel {
|
|
|
43
36
|
protected eventDispatcher: EventDispatcher,
|
|
44
37
|
protected injectorContext: InjectorContext,
|
|
45
38
|
protected logger: LoggerInterface,
|
|
46
|
-
protected stopwatch
|
|
39
|
+
protected stopwatch: Stopwatch,
|
|
47
40
|
) {
|
|
48
41
|
this.setHttpRequest = injectorContext.setter(undefined, HttpRequest);
|
|
49
42
|
this.setHttpResponse = injectorContext.setter(undefined, HttpResponse);
|
|
@@ -112,16 +105,12 @@ export class HttpKernel {
|
|
|
112
105
|
|
|
113
106
|
req.throwErrorOnNotFound = options.throwOnNotFound || false;
|
|
114
107
|
|
|
115
|
-
const frame = this.stopwatch
|
|
116
|
-
const workflow = httpWorkflow.create('start', this.eventDispatcher,
|
|
108
|
+
const frame = this.stopwatch.start(req.method + ' ' + req.getUrl(), FrameCategory.http, true);
|
|
109
|
+
const workflow = httpWorkflow.create('start', this.eventDispatcher, this.stopwatch, httpInjectorContext);
|
|
117
110
|
|
|
118
111
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
await frame.run(() => workflow.apply('request', new HttpRequestEvent(httpInjectorContext, req, res)));
|
|
122
|
-
} else {
|
|
123
|
-
await workflow.apply('request', new HttpRequestEvent(httpInjectorContext, req, res));
|
|
124
|
-
}
|
|
112
|
+
frame.data({ url: req.getUrl(), method: req.getMethod(), clientIp: req.getRemoteAddress() });
|
|
113
|
+
await frame.run(() => workflow.apply('request', new HttpRequestEvent(httpInjectorContext, req, res)));
|
|
125
114
|
} catch (error: any) {
|
|
126
115
|
if (!res.headersSent) {
|
|
127
116
|
const resultFormatter = httpInjectorContext.get(HttpResultFormatter);
|
|
@@ -151,10 +140,8 @@ export class HttpKernel {
|
|
|
151
140
|
});
|
|
152
141
|
}
|
|
153
142
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
frame.end();
|
|
157
|
-
}
|
|
143
|
+
frame.data({ responseStatus: res.statusCode });
|
|
144
|
+
frame.end();
|
|
158
145
|
}
|
|
159
146
|
}
|
|
160
147
|
}
|
package/src/middleware.ts
CHANGED
|
@@ -79,8 +79,8 @@ export class HttpMiddlewareApi {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
* When the middleware
|
|
83
|
-
*
|
|
82
|
+
* When the middleware neither calls next() nor ends the response within <timeout> milliseconds,
|
|
83
|
+
* the request fails with HttpGatewayTimeoutError (504) and a warning is logged.
|
|
84
84
|
*/
|
|
85
85
|
timeout(timeout: number) {
|
|
86
86
|
this.t.timeout = timeout;
|
package/src/mime.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Deepkit Framework
|
|
3
|
+
* Copyright (C) 2021 Deepkit UG, Marc J. Schmidt
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the MIT License.
|
|
7
|
+
*
|
|
8
|
+
* You should have received a copy of the MIT License along with this program.
|
|
9
|
+
*/
|
|
10
|
+
import { extname } from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* File extension to MIME type map of common web types.
|
|
14
|
+
* Add more types at runtime by setting `mimeTypes[extension] = type`.
|
|
15
|
+
*/
|
|
16
|
+
export const mimeTypes: { [extension: string]: string } = {
|
|
17
|
+
// text
|
|
18
|
+
html: 'text/html',
|
|
19
|
+
htm: 'text/html',
|
|
20
|
+
css: 'text/css',
|
|
21
|
+
js: 'text/javascript',
|
|
22
|
+
mjs: 'text/javascript',
|
|
23
|
+
cjs: 'application/node',
|
|
24
|
+
txt: 'text/plain',
|
|
25
|
+
text: 'text/plain',
|
|
26
|
+
csv: 'text/csv',
|
|
27
|
+
tsv: 'text/tab-separated-values',
|
|
28
|
+
md: 'text/markdown',
|
|
29
|
+
markdown: 'text/markdown',
|
|
30
|
+
yaml: 'text/yaml',
|
|
31
|
+
yml: 'text/yaml',
|
|
32
|
+
ics: 'text/calendar',
|
|
33
|
+
vtt: 'text/vtt',
|
|
34
|
+
appcache: 'text/cache-manifest',
|
|
35
|
+
// application
|
|
36
|
+
json: 'application/json',
|
|
37
|
+
map: 'application/json',
|
|
38
|
+
jsonld: 'application/ld+json',
|
|
39
|
+
webmanifest: 'application/manifest+json',
|
|
40
|
+
xml: 'application/xml',
|
|
41
|
+
xsl: 'application/xml',
|
|
42
|
+
rtf: 'application/rtf',
|
|
43
|
+
wasm: 'application/wasm',
|
|
44
|
+
pdf: 'application/pdf',
|
|
45
|
+
zip: 'application/zip',
|
|
46
|
+
gz: 'application/gzip',
|
|
47
|
+
tar: 'application/x-tar',
|
|
48
|
+
bz2: 'application/x-bzip2',
|
|
49
|
+
'7z': 'application/x-7z-compressed',
|
|
50
|
+
rar: 'application/vnd.rar',
|
|
51
|
+
xz: 'application/x-xz',
|
|
52
|
+
bin: 'application/octet-stream',
|
|
53
|
+
epub: 'application/epub+zip',
|
|
54
|
+
doc: 'application/msword',
|
|
55
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
56
|
+
xls: 'application/vnd.ms-excel',
|
|
57
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
58
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
59
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
60
|
+
odt: 'application/vnd.oasis.opendocument.text',
|
|
61
|
+
ods: 'application/vnd.oasis.opendocument.spreadsheet',
|
|
62
|
+
odp: 'application/vnd.oasis.opendocument.presentation',
|
|
63
|
+
m3u8: 'application/vnd.apple.mpegurl',
|
|
64
|
+
mpd: 'application/dash+xml',
|
|
65
|
+
// images
|
|
66
|
+
png: 'image/png',
|
|
67
|
+
apng: 'image/apng',
|
|
68
|
+
jpg: 'image/jpeg',
|
|
69
|
+
jpeg: 'image/jpeg',
|
|
70
|
+
jpe: 'image/jpeg',
|
|
71
|
+
gif: 'image/gif',
|
|
72
|
+
webp: 'image/webp',
|
|
73
|
+
avif: 'image/avif',
|
|
74
|
+
svg: 'image/svg+xml',
|
|
75
|
+
svgz: 'image/svg+xml',
|
|
76
|
+
ico: 'image/vnd.microsoft.icon',
|
|
77
|
+
bmp: 'image/bmp',
|
|
78
|
+
tif: 'image/tiff',
|
|
79
|
+
tiff: 'image/tiff',
|
|
80
|
+
heic: 'image/heic',
|
|
81
|
+
heif: 'image/heif',
|
|
82
|
+
jxl: 'image/jxl',
|
|
83
|
+
psd: 'image/vnd.adobe.photoshop',
|
|
84
|
+
// fonts
|
|
85
|
+
woff: 'font/woff',
|
|
86
|
+
woff2: 'font/woff2',
|
|
87
|
+
ttf: 'font/ttf',
|
|
88
|
+
otf: 'font/otf',
|
|
89
|
+
eot: 'application/vnd.ms-fontobject',
|
|
90
|
+
// audio
|
|
91
|
+
mp3: 'audio/mpeg',
|
|
92
|
+
m4a: 'audio/mp4',
|
|
93
|
+
aac: 'audio/aac',
|
|
94
|
+
oga: 'audio/ogg',
|
|
95
|
+
ogg: 'audio/ogg',
|
|
96
|
+
opus: 'audio/ogg',
|
|
97
|
+
wav: 'audio/wav',
|
|
98
|
+
flac: 'audio/x-flac',
|
|
99
|
+
// video
|
|
100
|
+
mp4: 'video/mp4',
|
|
101
|
+
m4v: 'video/x-m4v',
|
|
102
|
+
webm: 'video/webm',
|
|
103
|
+
ogv: 'video/ogg',
|
|
104
|
+
mov: 'video/quicktime',
|
|
105
|
+
avi: 'video/x-msvideo',
|
|
106
|
+
mkv: 'video/x-matroska',
|
|
107
|
+
mpeg: 'video/mpeg',
|
|
108
|
+
mpg: 'video/mpeg',
|
|
109
|
+
'3gp': 'video/3gpp',
|
|
110
|
+
ts: 'video/mp2t',
|
|
111
|
+
// 3d models
|
|
112
|
+
glb: 'model/gltf-binary',
|
|
113
|
+
gltf: 'model/gltf+json',
|
|
114
|
+
obj: 'model/obj',
|
|
115
|
+
stl: 'model/stl',
|
|
116
|
+
usdz: 'model/vnd.usdz+zip',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Returns the MIME type for a file path, file name, or extension (with or without leading dot).
|
|
121
|
+
* Returns undefined for unknown extensions.
|
|
122
|
+
*
|
|
123
|
+
* `getMimeType('/assets/logo.svg')` → `image/svg+xml`
|
|
124
|
+
*/
|
|
125
|
+
export function getMimeType(path: string): string | undefined {
|
|
126
|
+
const extension = extname('x.' + path)
|
|
127
|
+
.slice(1)
|
|
128
|
+
.toLowerCase();
|
|
129
|
+
return Object.prototype.hasOwnProperty.call(mimeTypes, extension) ? mimeTypes[extension] : undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Returns the value for a Content-Type header, including `charset=utf-8` for text types.
|
|
134
|
+
* Returns undefined for unknown extensions.
|
|
135
|
+
*/
|
|
136
|
+
export function getContentType(path: string): string | undefined {
|
|
137
|
+
const type = getMimeType(path);
|
|
138
|
+
if (!type) return;
|
|
139
|
+
const isText = type.startsWith('text/') || type === 'application/json' || type === 'application/manifest+json';
|
|
140
|
+
return isText ? type + '; charset=utf-8' : type;
|
|
141
|
+
}
|
package/src/model.ts
CHANGED
|
@@ -312,13 +312,21 @@ export class RequestBuilder {
|
|
|
312
312
|
}, this.contentBuffer);
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Replaces all headers. Names are lowercased like Node does for incoming requests.
|
|
317
|
+
*/
|
|
315
318
|
headers(headers: { [name: string]: string }): this {
|
|
316
|
-
this._headers =
|
|
319
|
+
this._headers = {};
|
|
320
|
+
for (const [name, value] of Object.entries(headers)) this.header(name, value);
|
|
317
321
|
return this;
|
|
318
322
|
}
|
|
319
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Sets a header. The name is lowercased like Node does for incoming requests,
|
|
326
|
+
* so `header('Authorization', ...)` is found by `authorization: HttpHeader<string>`.
|
|
327
|
+
*/
|
|
320
328
|
header(name: string, value: string | number): this {
|
|
321
|
-
this._headers[name] = String(value);
|
|
329
|
+
this._headers[name.toLowerCase()] = String(value);
|
|
322
330
|
return this;
|
|
323
331
|
}
|
|
324
332
|
|
package/src/module.ts
CHANGED
|
@@ -32,8 +32,9 @@ export class HttpModule extends createModuleClass({
|
|
|
32
32
|
HttpResultFormatter,
|
|
33
33
|
HttpRouterRegistry,
|
|
34
34
|
HttpRouterFilterResolver,
|
|
35
|
-
{ provide:
|
|
36
|
-
{ provide:
|
|
35
|
+
{ provide: InjectorContext, useValue: undefined, scope: 'http' },
|
|
36
|
+
{ provide: HttpResponse, useValue: undefined, scope: 'http' },
|
|
37
|
+
{ provide: HttpRequest, useValue: undefined, scope: 'http' },
|
|
37
38
|
{ provide: RouteConfig, useValue: undefined, scope: 'http' },
|
|
38
39
|
{ provide: Logger, useValue: new Logger([new ConsoleTransport()]) },
|
|
39
40
|
],
|
|
@@ -49,6 +50,7 @@ export class HttpModule extends createModuleClass({
|
|
|
49
50
|
HttpKernel,
|
|
50
51
|
HttpResultFormatter,
|
|
51
52
|
HttpRouterFilterResolver,
|
|
53
|
+
InjectorContext,
|
|
52
54
|
HttpResponse,
|
|
53
55
|
HttpRequest,
|
|
54
56
|
HttpControllers,
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Deepkit Framework
|
|
3
|
+
* Copyright (C) 2021 Deepkit UG, Marc J. Schmidt
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the MIT License.
|
|
7
|
+
*
|
|
8
|
+
* You should have received a copy of the MIT License along with this program.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Query string parser compatible with the default behavior of `qs.parse`.
|
|
13
|
+
*
|
|
14
|
+
* Supports nested objects (`a[b]=1`), arrays (`a[]=1`, `a[0]=1`) and duplicate keys (`a=1&a=2` becomes an array).
|
|
15
|
+
*
|
|
16
|
+
* Limits:
|
|
17
|
+
* - at most 1000 parameters are parsed, the rest is ignored
|
|
18
|
+
* - at most 5 levels of nesting, the remainder is used as literal key (`a[b][c][d][e][f][g]` → `a.b.c.d.e.f['[g]']`)
|
|
19
|
+
* - array indices above 20 create object keys instead of sparse arrays (`a[100]=1` → `{a: {100: '1'}}`)
|
|
20
|
+
*
|
|
21
|
+
* Keys that exist on Object.prototype (`__proto__`, `constructor`, `toString`, `hasOwnProperty`, ...) are dropped.
|
|
22
|
+
*/
|
|
23
|
+
const parameterLimit = 1000;
|
|
24
|
+
const depthLimit = 5;
|
|
25
|
+
const arrayLimit = 20;
|
|
26
|
+
|
|
27
|
+
const has = Object.prototype.hasOwnProperty;
|
|
28
|
+
const isArray = Array.isArray;
|
|
29
|
+
|
|
30
|
+
function decode(str: string): string {
|
|
31
|
+
if (str.indexOf('+') !== -1) str = str.replace(/\+/g, ' ');
|
|
32
|
+
if (str.indexOf('%') === -1) return str;
|
|
33
|
+
try {
|
|
34
|
+
return decodeURIComponent(str);
|
|
35
|
+
} catch {
|
|
36
|
+
return str;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Query strings without brackets, the common case: `a=1&b=2&a=3` → `{a: ['1', '3'], b: '2'}`.
|
|
42
|
+
* Same result as the general parser, without its nesting and compaction steps.
|
|
43
|
+
*/
|
|
44
|
+
function parseFlat(str: string): Record<string, any> {
|
|
45
|
+
const obj: Record<string, any> = {};
|
|
46
|
+
for (const part of str.split('&', parameterLimit)) {
|
|
47
|
+
const pos = part.indexOf('=');
|
|
48
|
+
const key = decode(pos === -1 ? part : part.slice(0, pos));
|
|
49
|
+
if (!key || has.call(Object.prototype, key)) continue;
|
|
50
|
+
const val = pos === -1 ? '' : decode(part.slice(pos + 1));
|
|
51
|
+
obj[key] = has.call(obj, key) ? ([] as any[]).concat(obj[key], val) : val;
|
|
52
|
+
}
|
|
53
|
+
return obj;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const nesting = /[[\]]|%5[bd]/i;
|
|
57
|
+
|
|
58
|
+
function parseValues(str: string): Record<string, any> {
|
|
59
|
+
const obj: Record<string, any> = Object.create(null);
|
|
60
|
+
const parts = str.replace(/%5B/gi, '[').replace(/%5D/gi, ']').split('&', parameterLimit);
|
|
61
|
+
|
|
62
|
+
for (const part of parts) {
|
|
63
|
+
const bracketEqualsPos = part.indexOf(']=');
|
|
64
|
+
const pos = bracketEqualsPos === -1 ? part.indexOf('=') : bracketEqualsPos + 1;
|
|
65
|
+
|
|
66
|
+
const key = decode(pos === -1 ? part : part.slice(0, pos));
|
|
67
|
+
const val = pos === -1 ? '' : decode(part.slice(pos + 1));
|
|
68
|
+
|
|
69
|
+
obj[key] = has.call(obj, key) ? ([] as any[]).concat(obj[key], val) : val;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return obj;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseObject(chain: string[], val: any): any {
|
|
76
|
+
let leaf = val;
|
|
77
|
+
|
|
78
|
+
for (let i = chain.length - 1; i >= 0; --i) {
|
|
79
|
+
let obj: any;
|
|
80
|
+
const root = chain[i];
|
|
81
|
+
|
|
82
|
+
if (root === '[]') {
|
|
83
|
+
obj = ([] as any[]).concat(leaf);
|
|
84
|
+
} else {
|
|
85
|
+
obj = {};
|
|
86
|
+
const cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root;
|
|
87
|
+
const index = parseInt(cleanRoot, 10);
|
|
88
|
+
if (
|
|
89
|
+
!isNaN(index) &&
|
|
90
|
+
root !== cleanRoot &&
|
|
91
|
+
String(index) === cleanRoot &&
|
|
92
|
+
index >= 0 &&
|
|
93
|
+
index <= arrayLimit
|
|
94
|
+
) {
|
|
95
|
+
obj = [];
|
|
96
|
+
obj[index] = leaf;
|
|
97
|
+
} else if (cleanRoot !== '__proto__') {
|
|
98
|
+
obj[cleanRoot] = leaf;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
leaf = obj;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return leaf;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseKeys(key: string, val: any): any {
|
|
109
|
+
if (!key) return;
|
|
110
|
+
|
|
111
|
+
const brackets = /(\[[^[\]]*])/;
|
|
112
|
+
const child = /(\[[^[\]]*])/g;
|
|
113
|
+
|
|
114
|
+
let segment = brackets.exec(key);
|
|
115
|
+
const parent = segment ? key.slice(0, segment.index) : key;
|
|
116
|
+
|
|
117
|
+
const keys: string[] = [];
|
|
118
|
+
if (parent) {
|
|
119
|
+
if (has.call(Object.prototype, parent)) return;
|
|
120
|
+
keys.push(parent);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let i = 0;
|
|
124
|
+
while ((segment = child.exec(key)) !== null && i < depthLimit) {
|
|
125
|
+
i += 1;
|
|
126
|
+
if (has.call(Object.prototype, segment[1].slice(1, -1))) return;
|
|
127
|
+
keys.push(segment[1]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// depth limit reached, the remainder is used as literal key
|
|
131
|
+
if (segment) keys.push('[' + key.slice(segment.index) + ']');
|
|
132
|
+
|
|
133
|
+
return parseObject(keys, val);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function arrayToObject(source: any[]): Record<string, any> {
|
|
137
|
+
const obj: Record<string, any> = {};
|
|
138
|
+
for (let i = 0; i < source.length; ++i) {
|
|
139
|
+
if (source[i] !== undefined) obj[i] = source[i];
|
|
140
|
+
}
|
|
141
|
+
return obj;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function merge(target: any, source: any): any {
|
|
145
|
+
if (!source) return target;
|
|
146
|
+
|
|
147
|
+
if (typeof source !== 'object') {
|
|
148
|
+
if (isArray(target)) {
|
|
149
|
+
target.push(source);
|
|
150
|
+
} else if (target && typeof target === 'object') {
|
|
151
|
+
if (!has.call(Object.prototype, source)) target[source] = true;
|
|
152
|
+
} else {
|
|
153
|
+
return [target, source];
|
|
154
|
+
}
|
|
155
|
+
return target;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!target || typeof target !== 'object') {
|
|
159
|
+
return [target].concat(source);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (isArray(target) && isArray(source)) {
|
|
163
|
+
source.forEach((item, i) => {
|
|
164
|
+
if (has.call(target, i)) {
|
|
165
|
+
const targetItem = target[i];
|
|
166
|
+
if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') {
|
|
167
|
+
target[i] = merge(targetItem, item);
|
|
168
|
+
} else {
|
|
169
|
+
target.push(item);
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
target[i] = item;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return target;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const mergeTarget = isArray(target) && !isArray(source) ? arrayToObject(target) : target;
|
|
179
|
+
for (const key of Object.keys(source)) {
|
|
180
|
+
mergeTarget[key] = has.call(mergeTarget, key) ? merge(mergeTarget[key], source[key]) : source[key];
|
|
181
|
+
}
|
|
182
|
+
return mergeTarget;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Removes holes from (sparse) arrays, e.g. `a[1]=x` results in `['x']`, not `[undefined, 'x']`.
|
|
187
|
+
*/
|
|
188
|
+
function compact(value: Record<string, any>): Record<string, any> {
|
|
189
|
+
const queue: { obj: any; prop: string }[] = [{ obj: { o: value }, prop: 'o' }];
|
|
190
|
+
const refs: any[] = [];
|
|
191
|
+
|
|
192
|
+
for (let i = 0; i < queue.length; ++i) {
|
|
193
|
+
const obj = queue[i].obj[queue[i].prop];
|
|
194
|
+
for (const key of Object.keys(obj)) {
|
|
195
|
+
const val = obj[key];
|
|
196
|
+
if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) {
|
|
197
|
+
queue.push({ obj, prop: key });
|
|
198
|
+
refs.push(val);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
while (queue.length > 1) {
|
|
204
|
+
const item = queue.pop()!;
|
|
205
|
+
const obj = item.obj[item.prop];
|
|
206
|
+
if (isArray(obj)) item.obj[item.prop] = obj.filter(v => v !== undefined);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Parses a query string (without leading `?`) into an object.
|
|
214
|
+
*
|
|
215
|
+
* `a=1&b[c]=2&d[]=3&d[]=4` → `{a: '1', b: {c: '2'}, d: ['3', '4']}`
|
|
216
|
+
*/
|
|
217
|
+
export function parseQueryString(str: string): Record<string, any> {
|
|
218
|
+
if (!str) return {};
|
|
219
|
+
if (!nesting.test(str)) return parseFlat(str);
|
|
220
|
+
|
|
221
|
+
const values = parseValues(str);
|
|
222
|
+
let obj: Record<string, any> = {};
|
|
223
|
+
for (const key of Object.keys(values)) {
|
|
224
|
+
obj = merge(obj, parseKeys(key, values[key]));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return compact(obj);
|
|
228
|
+
}
|
package/src/request-parser.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from '@deepkit/type';
|
|
20
20
|
import { BodyValidationError, createRequestWithCachedBody, getRegExp, HttpRequest, ValidatedBody } from './model.js';
|
|
21
21
|
import { getRouteActionLabel, RouteConfig, UploadedFile, UploadedFileSymbol } from './router.js';
|
|
22
|
-
import
|
|
22
|
+
import { parseQueryString } from './query-string.js';
|
|
23
23
|
import formidable, { Fields, Files, Options } from 'formidable';
|
|
24
24
|
import { HttpParserOptions } from './module.config.js';
|
|
25
25
|
import type IncomingForm from 'formidable/Formidable.js';
|
|
@@ -231,10 +231,10 @@ export function buildRequestParser(parseOptions: HttpParserOptions, parameters:
|
|
|
231
231
|
routeConfig
|
|
232
232
|
});
|
|
233
233
|
compiler.context.set('ValidationError', ValidationError);
|
|
234
|
-
compiler.context.set('
|
|
234
|
+
compiler.context.set('parseQueryString', parseQueryString);
|
|
235
235
|
|
|
236
236
|
let needsQueryString = !!params.find(v => v.query || v.queries || v.requestParser);
|
|
237
|
-
const query = needsQueryString ? '_qPosition === -1 ? {} :
|
|
237
|
+
const query = needsQueryString ? '_qPosition === -1 ? {} : parseQueryString(_url.substr(_qPosition + 1))' : '{}';
|
|
238
238
|
|
|
239
239
|
const regexVar = compiler.reserveVariable('regex', new RegExp('^' + pathRegex + '$'));
|
|
240
240
|
|
|
@@ -329,7 +329,9 @@ export function getRequestParserCodeForParameters(
|
|
|
329
329
|
|
|
330
330
|
const queryPath = parameter.typePath === undefined && !parameter.queries ? parameter.parameter.name : parameter.typePath;
|
|
331
331
|
const accessor = queryPath ? `['` + (queryPath.replace(/\./g, `']['`)) + `']` : '';
|
|
332
|
-
|
|
332
|
+
// header names are case-insensitive, Node lowercases them in request.headers
|
|
333
|
+
const headerAccessor = queryPath ? `_headers[${JSON.stringify(queryPath.toLowerCase())}]` : '_headers';
|
|
334
|
+
const queryAccessor = parameter.header ? headerAccessor : queryPath ? `_query${accessor}` : '_query';
|
|
333
335
|
|
|
334
336
|
if (isOptional(parameter.parameter.parameter) || hasDefaultValue(parameter.parameter.parameter)) {
|
|
335
337
|
setParameters.push(`parameters.${parameter.parameter.name} = ${queryAccessor} === undefined ? undefined : ${converterVar}(${queryAccessor}, {loosely: true});`);
|