@znight/http 0.1.2 → 0.1.4
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/README.md +154 -35
- package/dist/compiler.d.mts +77 -2
- package/dist/compiler.mjs +259 -101
- package/dist/compiler.mjs.map +1 -1
- package/dist/express.mjs +3 -2
- package/dist/express.mjs.map +1 -1
- package/dist/fastify.mjs +200 -23
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.mjs +174 -43
- package/dist/hono.mjs.map +1 -1
- package/dist/index.d.mts +36 -4
- package/dist/index.mjs +1005 -257
- package/dist/index.mjs.map +1 -1
- package/dist/{node-Clm1Ia7T.mjs → node-DnYv_AMI.mjs} +4 -3
- package/dist/{node-Clm1Ia7T.mjs.map → node-DnYv_AMI.mjs.map} +1 -1
- package/dist/{node-io-CVnqpVNS.mjs → node-io-BkdRSQeg.mjs} +99 -60
- package/dist/{node-io-CVnqpVNS.mjs.map → node-io-BkdRSQeg.mjs.map} +1 -1
- package/dist/node.mjs +1 -1
- package/dist/utils-DIWdzVht.mjs +46 -0
- package/dist/utils-DIWdzVht.mjs.map +1 -0
- package/package.json +4 -4
- package/src/adapters/express.ts +4 -1
- package/src/adapters/fastify.ts +265 -25
- package/src/adapters/hono.ts +225 -58
- package/src/adapters/node-io.ts +128 -58
- package/src/adapters/node.ts +4 -1
- package/src/body.ts +31 -2
- package/src/coerce.ts +31 -0
- package/src/compiler/analyze.ts +3 -3
- package/src/compiler/generate.ts +127 -22
- package/src/compiler/index.ts +13 -0
- package/src/compiler/route-decorators.ts +45 -2
- package/src/compiler/route-groups.ts +216 -0
- package/src/compiler/validate.ts +15 -126
- package/src/context.ts +148 -78
- package/src/decorators.ts +19 -3
- package/src/index.ts +2 -1
- package/src/pipeline.ts +936 -177
- package/src/plugin-routes.ts +19 -0
- package/src/plugin.ts +7 -3
- package/src/response.ts +6 -8
- package/src/serialize.ts +48 -21
- package/src/utils.ts +25 -2
- package/src/validate.ts +28 -12
- package/dist/utils-DCbu3a4w.mjs +0 -27
- package/dist/utils-DCbu3a4w.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { n as RouteTable, t as nodeAdapter } from "./node-
|
|
2
|
-
import { $ as
|
|
1
|
+
import { n as RouteTable, t as nodeAdapter } from "./node-DnYv_AMI.mjs";
|
|
2
|
+
import { $ as TooManyRequestsException, A as parseSetCookie, B as BadGatewayException, C as DEFAULT_COOKIE_OPTIONS, D as givenCookieKey, E as encodeCookieValue, F as discardStream, G as GoneException, H as ConflictException, I as isByteStream, J as MethodNotAllowedException, K as HttpException, L as isStreamSource, M as serializeCookie, N as serializeResponseCookie, O as isCookieName, P as captureContext, Q as ServiceUnavailableException, R as toByteStream, T as decodeCookieValue, U as ForbiddenException, V as BadRequestException, W as GatewayTimeoutException, X as NotImplementedException, Y as NotFoundException, Z as PayloadTooLargeException, at as issue, ct as isValidStatus, et as UnauthorizedException, g as isEventStreamType, it as isHttpException, j as serializeClearCookie, k as parseCookieHeader, lt as statusText, nt as UnsupportedMediaTypeException, ot as toIssueDetail, q as InternalServerErrorException, rt as ValidationException, st as isBodylessStatus, tt as UnprocessableEntityException, w as checkCookie, z as trackStream } from "./node-io-BkdRSQeg.mjs";
|
|
3
3
|
import { a as isMediaTypePattern, i as contentTypeFor, n as parseByteSize, o as matchesMediaType, r as OCTET_STREAM, s as mediaTypeEssence, t as formatByteSize } from "./size-D8rsQcV0.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { a as isThenable, i as isBun, n as errorMessage, r as fastRecord, t as emptyRecord } from "./utils-DIWdzVht.mjs";
|
|
5
5
|
import { RoutePathError, compareRoutePaths, compareStrings, decodePathValue, joinPaths, matchRoutePath, normalizePath, parseRoutePath } from "./path.mjs";
|
|
6
6
|
import { Readable } from "node:stream";
|
|
7
7
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
@@ -483,9 +483,35 @@ function splitUrl(url) {
|
|
|
483
483
|
}
|
|
484
484
|
// src/body.ts
|
|
485
485
|
const UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
486
|
+
/**
|
|
487
|
+
* Parsed `content-type` values: an application receives a handful of them, parsed twice per request
|
|
488
|
+
* with a body (request context, then {@link parseBody}). Bounded, since clients choose the values:
|
|
489
|
+
* cleared when full.
|
|
490
|
+
*/
|
|
491
|
+
const MEDIA_TYPES = /* @__PURE__ */ new Map();
|
|
492
|
+
const MEDIA_TYPES_MAX = 64;
|
|
486
493
|
/** Parses a `content-type` header value (`application/json; charset=utf-8`). */
|
|
487
494
|
function parseMediaType(header) {
|
|
488
495
|
if (header === void 0) return void 0;
|
|
496
|
+
if (header === "application/json") return {
|
|
497
|
+
type: "application/json",
|
|
498
|
+
charset: void 0
|
|
499
|
+
};
|
|
500
|
+
const cached = MEDIA_TYPES.get(header);
|
|
501
|
+
if (cached !== void 0) return {
|
|
502
|
+
type: cached.type,
|
|
503
|
+
charset: cached.charset
|
|
504
|
+
};
|
|
505
|
+
const media = parseUncached(header);
|
|
506
|
+
if (media === void 0) return void 0;
|
|
507
|
+
if (MEDIA_TYPES.size === MEDIA_TYPES_MAX) MEDIA_TYPES.clear();
|
|
508
|
+
MEDIA_TYPES.set(header, media);
|
|
509
|
+
return {
|
|
510
|
+
type: media.type,
|
|
511
|
+
charset: media.charset
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function parseUncached(header) {
|
|
489
515
|
const separator = header.indexOf(";");
|
|
490
516
|
const type = (separator === -1 ? header : header.slice(0, separator)).trim().toLowerCase();
|
|
491
517
|
if (type === "") return void 0;
|
|
@@ -537,12 +563,16 @@ function hasBody(headers) {
|
|
|
537
563
|
*/
|
|
538
564
|
function parseBody(bytes, contentType, contentEncoding) {
|
|
539
565
|
try {
|
|
540
|
-
return Promise.resolve(
|
|
566
|
+
return Promise.resolve(parseBodyBytes(bytes, contentType, contentEncoding));
|
|
541
567
|
} catch (error) {
|
|
542
568
|
return Promise.reject(error);
|
|
543
569
|
}
|
|
544
570
|
}
|
|
545
|
-
|
|
571
|
+
/**
|
|
572
|
+
* {@link parseBody} without the promise: returns the parsed body or throws its `HttpException`
|
|
573
|
+
* (internal: the request context parses the buffered body with it, without an extra microtask).
|
|
574
|
+
*/
|
|
575
|
+
function parseBodyBytes(bytes, contentType, contentEncoding) {
|
|
546
576
|
if (bytes.byteLength === 0) return void 0;
|
|
547
577
|
checkContentEncoding(contentEncoding);
|
|
548
578
|
const media = parseMediaType(contentType);
|
|
@@ -601,7 +631,29 @@ function single(value, label) {
|
|
|
601
631
|
if (Array.isArray(value)) throw new BadRequestException(`Invalid ${label}: expected a single value, received ${value.length}`);
|
|
602
632
|
throw invalid$1(label, "a string", value);
|
|
603
633
|
}
|
|
634
|
+
/** Longest run of decimal digits whose value is exact without parsing (`999999999999999` < 2^53). */
|
|
635
|
+
const MAX_FAST_DIGITS = 15;
|
|
636
|
+
/**
|
|
637
|
+
* The value of `text` when it is 1 to 15 ASCII decimal digits (the common `/users/42`), `-1`
|
|
638
|
+
* otherwise. The same number as `Number(text)` for such a string, without the regular expression,
|
|
639
|
+
* the trim and the parse of the general path.
|
|
640
|
+
*/
|
|
641
|
+
function digitsValue(text) {
|
|
642
|
+
const length = text.length;
|
|
643
|
+
if (length === 0 || length > MAX_FAST_DIGITS) return -1;
|
|
644
|
+
let result = 0;
|
|
645
|
+
for (let index = 0; index < length; index++) {
|
|
646
|
+
const digit = text.charCodeAt(index) - 48;
|
|
647
|
+
if (digit < 0 || digit > 9) return -1;
|
|
648
|
+
result = result * 10 + digit;
|
|
649
|
+
}
|
|
650
|
+
return result;
|
|
651
|
+
}
|
|
604
652
|
function toNumber(value, label) {
|
|
653
|
+
if (typeof value === "string") {
|
|
654
|
+
const digits = digitsValue(value);
|
|
655
|
+
if (digits !== -1) return digits;
|
|
656
|
+
}
|
|
605
657
|
const raw = single(value, label);
|
|
606
658
|
const text = raw.trim();
|
|
607
659
|
if (NUMBER.test(text)) {
|
|
@@ -611,6 +663,10 @@ function toNumber(value, label) {
|
|
|
611
663
|
throw invalid$1(label, "a number", raw);
|
|
612
664
|
}
|
|
613
665
|
function toInteger(value, label) {
|
|
666
|
+
if (typeof value === "string") {
|
|
667
|
+
const digits = digitsValue(value);
|
|
668
|
+
if (digits !== -1) return digits;
|
|
669
|
+
}
|
|
614
670
|
const raw = single(value, label);
|
|
615
671
|
const text = raw.trim();
|
|
616
672
|
if (INTEGER.test(text)) {
|
|
@@ -620,6 +676,8 @@ function toInteger(value, label) {
|
|
|
620
676
|
throw invalid$1(label, "an integer", raw);
|
|
621
677
|
}
|
|
622
678
|
function toBoolean(value, label) {
|
|
679
|
+
if (value === "true" || value === "1") return true;
|
|
680
|
+
if (value === "false" || value === "0") return false;
|
|
623
681
|
const raw = single(value, label);
|
|
624
682
|
switch (raw.trim().toLowerCase()) {
|
|
625
683
|
case "true":
|
|
@@ -781,8 +839,27 @@ const ABORT_MESSAGES = {
|
|
|
781
839
|
shutdown: "The server is shutting down",
|
|
782
840
|
expired: "The event stream reached its maxDuration"
|
|
783
841
|
};
|
|
842
|
+
/**
|
|
843
|
+
* The state of a request context that most requests never need (parsed query, `ctx.state`, body,
|
|
844
|
+
* request cookies, abort signal), created on first use: a context is created for every request.
|
|
845
|
+
*/
|
|
846
|
+
var ContextState = class {
|
|
847
|
+
query = void 0;
|
|
848
|
+
state = void 0;
|
|
849
|
+
body = void 0;
|
|
850
|
+
files = void 0;
|
|
851
|
+
raw = void 0;
|
|
852
|
+
streamed = false;
|
|
853
|
+
/** Stops the body stream opened for the request, unless it was read to its end or cancelled. */
|
|
854
|
+
stopBody = void 0;
|
|
855
|
+
parsed = void 0;
|
|
856
|
+
cookies = void 0;
|
|
857
|
+
signed = void 0;
|
|
858
|
+
abortReason = void 0;
|
|
859
|
+
abortController = void 0;
|
|
860
|
+
};
|
|
784
861
|
/** Implementation of {@link HttpContext} used by the pipeline (internal). */
|
|
785
|
-
var RequestContext = class {
|
|
862
|
+
var RequestContext = class RequestContext {
|
|
786
863
|
requestId;
|
|
787
864
|
method;
|
|
788
865
|
url;
|
|
@@ -798,60 +875,64 @@ var RequestContext = class {
|
|
|
798
875
|
#source;
|
|
799
876
|
#plan;
|
|
800
877
|
#rawQuery;
|
|
801
|
-
#query;
|
|
802
|
-
#state;
|
|
803
|
-
#responseHeaders;
|
|
804
|
-
#body;
|
|
805
|
-
#files;
|
|
806
|
-
#raw;
|
|
807
|
-
#streamed = false;
|
|
808
|
-
/** Stops the body stream opened for the request, unless it was read to its end or cancelled. */
|
|
809
|
-
#stopBody;
|
|
810
|
-
#parsed;
|
|
811
878
|
#cookieSettings;
|
|
812
|
-
#
|
|
813
|
-
#signed;
|
|
879
|
+
#responseHeaders = void 0;
|
|
814
880
|
/** Cookies set on the response, by key (name, domain, path), with their `Set-Cookie` line. */
|
|
815
|
-
#jar;
|
|
881
|
+
#jar = void 0;
|
|
816
882
|
/** The response headers were handed to the adapter (cookies can no longer be set). */
|
|
817
883
|
#committed = false;
|
|
818
884
|
/** The request is over: later aborts are ignored. */
|
|
819
885
|
#completed = false;
|
|
820
|
-
#
|
|
821
|
-
|
|
822
|
-
constructor(source,
|
|
886
|
+
#more = void 0;
|
|
887
|
+
/** `path` and `query`: `source.url` split; `client`: see {@link RequestContextInit.client}. */
|
|
888
|
+
constructor(source, requestId, adapter, path, query, route, params, body, cookies, client) {
|
|
823
889
|
this.#source = source;
|
|
824
|
-
this.#plan =
|
|
825
|
-
this.#cookieSettings =
|
|
826
|
-
const { path, query } = init.url ?? splitUrl(source.url);
|
|
890
|
+
this.#plan = body;
|
|
891
|
+
this.#cookieSettings = cookies;
|
|
827
892
|
this.#rawQuery = query;
|
|
828
|
-
this.requestId =
|
|
893
|
+
this.requestId = requestId;
|
|
829
894
|
this.method = source.method;
|
|
830
895
|
this.url = source.url;
|
|
831
896
|
this.path = path;
|
|
832
|
-
this.params =
|
|
897
|
+
this.params = params;
|
|
833
898
|
this.headers = source.headers;
|
|
834
|
-
this.route =
|
|
899
|
+
this.route = route;
|
|
835
900
|
this.request = source.raw;
|
|
836
901
|
this.response = source.rawResponse;
|
|
837
|
-
this.adapter =
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
902
|
+
this.adapter = adapter;
|
|
903
|
+
if (client === void 0) {
|
|
904
|
+
this.ip = source.ip;
|
|
905
|
+
this.protocol = source.encrypted === true ? "https" : "http";
|
|
906
|
+
} else {
|
|
907
|
+
this.ip = client.ip;
|
|
908
|
+
this.protocol = client.protocol;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
/** A context from named settings (defaults: `source.url` split, secure cookie defaults, the adapter's client). */
|
|
912
|
+
static of(source, init) {
|
|
913
|
+
const { path, query } = init.url ?? splitUrl(source.url);
|
|
914
|
+
return new RequestContext(source, init.requestId, init.adapter, path, query, init.route, init.params, init.body, init.cookies ?? DEFAULT_COOKIE_SETTINGS, init.client);
|
|
915
|
+
}
|
|
916
|
+
/** The state created on first use. */
|
|
917
|
+
get #extra() {
|
|
918
|
+
this.#more ??= new ContextState();
|
|
919
|
+
return this.#more;
|
|
841
920
|
}
|
|
842
921
|
get query() {
|
|
843
|
-
|
|
844
|
-
|
|
922
|
+
const state = this.#extra;
|
|
923
|
+
state.query ??= parseQuery(this.#rawQuery);
|
|
924
|
+
return state.query;
|
|
845
925
|
}
|
|
846
926
|
get state() {
|
|
847
|
-
|
|
848
|
-
|
|
927
|
+
const state = this.#extra;
|
|
928
|
+
state.state ??= /* @__PURE__ */ new Map();
|
|
929
|
+
return state.state;
|
|
849
930
|
}
|
|
850
931
|
get body() {
|
|
851
|
-
return this.#body;
|
|
932
|
+
return this.#more?.body;
|
|
852
933
|
}
|
|
853
934
|
get files() {
|
|
854
|
-
return this.#files ?? NO_FILES$1;
|
|
935
|
+
return this.#more?.files ?? NO_FILES$1;
|
|
855
936
|
}
|
|
856
937
|
/** Maximum body size of the route, in bytes. */
|
|
857
938
|
get bodyLimit() {
|
|
@@ -879,16 +960,18 @@ var RequestContext = class {
|
|
|
879
960
|
else this.#responseHeaders[key] = typeof existing === "string" ? [existing, value] : [...existing, value];
|
|
880
961
|
}
|
|
881
962
|
get cookies() {
|
|
882
|
-
|
|
883
|
-
|
|
963
|
+
const state = this.#extra;
|
|
964
|
+
state.cookies ??= parseCookieHeader(this.headers["cookie"]);
|
|
965
|
+
return state.cookies;
|
|
884
966
|
}
|
|
885
967
|
signedCookie(name) {
|
|
886
968
|
const signer = this.#cookieSettings.signer;
|
|
887
969
|
if (signer === void 0) throw missingSecrets(`Cannot read the signed cookie ${JSON.stringify(String(name))}`);
|
|
888
|
-
|
|
889
|
-
|
|
970
|
+
const state = this.#extra;
|
|
971
|
+
state.signed ??= /* @__PURE__ */ new Map();
|
|
972
|
+
if (state.signed.has(name)) return state.signed.get(name);
|
|
890
973
|
const value = verifySigned(signer, name, this.cookies[name]);
|
|
891
|
-
|
|
974
|
+
state.signed.set(name, value);
|
|
892
975
|
return value;
|
|
893
976
|
}
|
|
894
977
|
setCookie(name, value, options = {}) {
|
|
@@ -908,6 +991,10 @@ var RequestContext = class {
|
|
|
908
991
|
get responseCookies() {
|
|
909
992
|
return this.#jar === void 0 ? NO_COOKIES$1 : [...this.#jar.values()].map((entry) => entry.cookie);
|
|
910
993
|
}
|
|
994
|
+
/** Whether {@link setCookie} / {@link clearCookie} were called (internal). */
|
|
995
|
+
get hasCookies() {
|
|
996
|
+
return this.#jar !== void 0;
|
|
997
|
+
}
|
|
911
998
|
/** The `Set-Cookie` lines of {@link setCookie} / {@link clearCookie}, with their keys, in order (internal). */
|
|
912
999
|
get cookieLines() {
|
|
913
1000
|
return this.#jar === void 0 ? NO_LINES : [...this.#jar].map(([key, entry]) => ({
|
|
@@ -932,27 +1019,29 @@ var RequestContext = class {
|
|
|
932
1019
|
});
|
|
933
1020
|
}
|
|
934
1021
|
get signal() {
|
|
935
|
-
|
|
1022
|
+
const state = this.#extra;
|
|
1023
|
+
if (state.abortController === void 0) {
|
|
936
1024
|
const controller = new AbortController();
|
|
937
|
-
|
|
938
|
-
if (
|
|
1025
|
+
state.abortController = controller;
|
|
1026
|
+
if (state.abortReason !== void 0) controller.abort(state.abortReason);
|
|
939
1027
|
else if (!this.#completed) this.#source.onAbort?.(() => this.abort("client"));
|
|
940
1028
|
}
|
|
941
|
-
return
|
|
1029
|
+
return state.abortController.signal;
|
|
942
1030
|
}
|
|
943
1031
|
/** Whether {@link signal} aborted (or will be created aborted). */
|
|
944
1032
|
get aborted() {
|
|
945
|
-
return this.#abortReason !== void 0;
|
|
1033
|
+
return this.#more?.abortReason !== void 0;
|
|
946
1034
|
}
|
|
947
1035
|
/**
|
|
948
1036
|
* Aborts {@link signal} with a {@link RequestAbortedError} of `kind` (internal): at most once, and
|
|
949
1037
|
* never once the request is over ({@link complete}).
|
|
950
1038
|
*/
|
|
951
1039
|
abort(kind) {
|
|
952
|
-
if (this.#abortReason !== void 0 || this.#completed) return;
|
|
1040
|
+
if (this.#more?.abortReason !== void 0 || this.#completed) return;
|
|
953
1041
|
const reason = new RequestAbortedError(kind);
|
|
954
|
-
|
|
955
|
-
|
|
1042
|
+
const state = this.#extra;
|
|
1043
|
+
state.abortReason = reason;
|
|
1044
|
+
state.abortController?.abort(reason);
|
|
956
1045
|
}
|
|
957
1046
|
/** The response headers are handed to the adapter: cookies can no longer be set (internal). */
|
|
958
1047
|
commit() {
|
|
@@ -963,15 +1052,17 @@ var RequestContext = class {
|
|
|
963
1052
|
this.#completed = true;
|
|
964
1053
|
}
|
|
965
1054
|
rawBody() {
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1055
|
+
const state = this.#extra;
|
|
1056
|
+
if (state.raw === void 0) {
|
|
1057
|
+
if (state.streamed) return Promise.reject(alreadyStreamed());
|
|
1058
|
+
state.raw = this.#source.readBody(this.#plan.limit);
|
|
969
1059
|
}
|
|
970
|
-
return
|
|
1060
|
+
return state.raw;
|
|
971
1061
|
}
|
|
972
1062
|
bodyStream() {
|
|
973
|
-
|
|
974
|
-
|
|
1063
|
+
const state = this.#extra;
|
|
1064
|
+
if (state.raw !== void 0 || state.streamed) throw alreadyStreamed();
|
|
1065
|
+
state.streamed = true;
|
|
975
1066
|
return this.#openBodyStream();
|
|
976
1067
|
}
|
|
977
1068
|
/**
|
|
@@ -980,18 +1071,21 @@ var RequestContext = class {
|
|
|
980
1071
|
* adapter discard the rest of the body, so that the connection can serve the next request.
|
|
981
1072
|
*/
|
|
982
1073
|
releaseBody() {
|
|
983
|
-
const
|
|
984
|
-
|
|
1074
|
+
const state = this.#more;
|
|
1075
|
+
if (state === void 0) return;
|
|
1076
|
+
const stop = state.stopBody;
|
|
1077
|
+
state.stopBody = void 0;
|
|
985
1078
|
stop?.();
|
|
986
1079
|
}
|
|
987
1080
|
/** The body as a stream (see {@link releaseBody}). */
|
|
988
1081
|
#openBodyStream() {
|
|
989
1082
|
const reader = openBodyStream(this.#source, this.#plan.limit).getReader();
|
|
1083
|
+
const state = this.#extra;
|
|
990
1084
|
let controller;
|
|
991
1085
|
const settle = () => {
|
|
992
|
-
|
|
1086
|
+
state.stopBody = void 0;
|
|
993
1087
|
};
|
|
994
|
-
|
|
1088
|
+
state.stopBody = () => {
|
|
995
1089
|
reader.cancel().catch(() => void 0);
|
|
996
1090
|
try {
|
|
997
1091
|
controller?.error(/* @__PURE__ */ new Error("The request is over: its body can no longer be read"));
|
|
@@ -1023,32 +1117,34 @@ var RequestContext = class {
|
|
|
1023
1117
|
}, { highWaterMark: 0 });
|
|
1024
1118
|
}
|
|
1025
1119
|
parseBody() {
|
|
1026
|
-
|
|
1027
|
-
|
|
1120
|
+
const state = this.#extra;
|
|
1121
|
+
state.parsed ??= this.#parse();
|
|
1122
|
+
return state.parsed;
|
|
1028
1123
|
}
|
|
1029
1124
|
async #parse() {
|
|
1030
1125
|
const headers = this.headers;
|
|
1031
1126
|
const contentType = headers["content-type"];
|
|
1032
1127
|
const media = parseMediaType(contentType);
|
|
1128
|
+
const state = this.#extra;
|
|
1033
1129
|
if (media?.type === "multipart/form-data") {
|
|
1034
1130
|
checkContentEncoding(headers["content-encoding"]);
|
|
1035
1131
|
const policy = this.#plan.multipart;
|
|
1036
1132
|
let result;
|
|
1037
|
-
if (
|
|
1038
|
-
if (
|
|
1039
|
-
|
|
1133
|
+
if (state.raw === void 0) {
|
|
1134
|
+
if (state.streamed) throw alreadyStreamed();
|
|
1135
|
+
state.streamed = true;
|
|
1040
1136
|
result = await readMultipart(this.#openBodyStream(), contentType, policy);
|
|
1041
|
-
} else result = parseMultipart(await
|
|
1042
|
-
|
|
1043
|
-
|
|
1137
|
+
} else result = parseMultipart(await state.raw, contentType, policy);
|
|
1138
|
+
state.files = result.files;
|
|
1139
|
+
state.body = result.fields;
|
|
1044
1140
|
return result.fields;
|
|
1045
1141
|
}
|
|
1046
1142
|
if (this.#plan.upload) {
|
|
1047
1143
|
if (!(headers["content-length"] !== void 0 || headers["transfer-encoding"] !== void 0 ? hasBody(headers) : this.#source.hasBody?.() === true)) return void 0;
|
|
1048
1144
|
throw new UnsupportedMediaTypeException(`Expected a multipart/form-data body${media === void 0 ? "" : ` (received ${JSON.stringify(media.type)})`}`);
|
|
1049
1145
|
}
|
|
1050
|
-
const body =
|
|
1051
|
-
|
|
1146
|
+
const body = parseBodyBytes(await this.rawBody(), contentType, headers["content-encoding"]);
|
|
1147
|
+
state.body = body;
|
|
1052
1148
|
return body;
|
|
1053
1149
|
}
|
|
1054
1150
|
};
|
|
@@ -1788,7 +1884,7 @@ function encodeExtValue(value) {
|
|
|
1788
1884
|
*/
|
|
1789
1885
|
/** Length of a signature: base64url of 32 bytes, without padding. */
|
|
1790
1886
|
const SIGNATURE_LENGTH = 43;
|
|
1791
|
-
const ENCODER$
|
|
1887
|
+
const ENCODER$3 = new TextEncoder();
|
|
1792
1888
|
/**
|
|
1793
1889
|
* Signs and verifies cookie values. The wire form of a signed cookie is
|
|
1794
1890
|
* `<encoded value>.<signature>` with `signature = base64url(HMAC-SHA256(secret, "<name>=<encoded value>"))`:
|
|
@@ -1835,7 +1931,7 @@ function isSignature(text) {
|
|
|
1835
1931
|
return true;
|
|
1836
1932
|
}
|
|
1837
1933
|
function toBytes(secret) {
|
|
1838
|
-
return typeof secret === "string" ? ENCODER$
|
|
1934
|
+
return typeof secret === "string" ? ENCODER$3.encode(secret) : secret;
|
|
1839
1935
|
}
|
|
1840
1936
|
/**
|
|
1841
1937
|
* Normalises the option `cookies.secrets` to a list, or explains what is wrong with it (a secret
|
|
@@ -2851,8 +2947,8 @@ function describe$1(value) {
|
|
|
2851
2947
|
function errorText(error) {
|
|
2852
2948
|
return error instanceof Error ? error.message : String(error);
|
|
2853
2949
|
}
|
|
2854
|
-
const ENCODER$
|
|
2855
|
-
const HEARTBEAT = ENCODER$
|
|
2950
|
+
const ENCODER$2 = new TextEncoder();
|
|
2951
|
+
const HEARTBEAT = ENCODER$2.encode(":\n\n");
|
|
2856
2952
|
/**
|
|
2857
2953
|
* One event stream: the byte stream sent to the client and the controls of the pipeline
|
|
2858
2954
|
* ({@link close} on shutdown or `maxDuration`). The source is read only when the client pulls
|
|
@@ -2902,7 +2998,7 @@ var SseSession = class {
|
|
|
2902
2998
|
start: (controller) => {
|
|
2903
2999
|
this.#controller = controller;
|
|
2904
3000
|
const retry = settings.retry;
|
|
2905
|
-
controller.enqueue(retry === void 0 ? HEARTBEAT : ENCODER$
|
|
3001
|
+
controller.enqueue(retry === void 0 ? HEARTBEAT : ENCODER$2.encode(`retry: ${retry}\n\n`));
|
|
2906
3002
|
if (settings.maxDuration !== void 0) this.#maxTimer = unref(setTimeout(() => this.close("expired"), timerDelay(settings.maxDuration)));
|
|
2907
3003
|
},
|
|
2908
3004
|
pull: (controller) => this.#pull(controller),
|
|
@@ -2965,7 +3061,7 @@ var SseSession = class {
|
|
|
2965
3061
|
await this.#finish(true);
|
|
2966
3062
|
return;
|
|
2967
3063
|
}
|
|
2968
|
-
controller.enqueue(ENCODER$
|
|
3064
|
+
controller.enqueue(ENCODER$2.encode(encoded));
|
|
2969
3065
|
this.#atBoundary = true;
|
|
2970
3066
|
this.#lineHasContent = false;
|
|
2971
3067
|
this.#afterCr = false;
|
|
@@ -3093,7 +3189,7 @@ var SseSession = class {
|
|
|
3093
3189
|
#finalRetry() {
|
|
3094
3190
|
const base = this.#settings.retry ?? 3e3;
|
|
3095
3191
|
const random = this.#settings.random ?? Math.random;
|
|
3096
|
-
return ENCODER$
|
|
3192
|
+
return ENCODER$2.encode(`retry: ${Math.round(base + random() * base)}\n\n`);
|
|
3097
3193
|
}
|
|
3098
3194
|
/**
|
|
3099
3195
|
* Stops the source (`return()` / `cancel()`) and waits for it at most `stopTimeoutMs`: an async
|
|
@@ -3344,15 +3440,19 @@ function withCookie(cookies, cookie) {
|
|
|
3344
3440
|
}
|
|
3345
3441
|
/** Whether `value` is an {@link HttpResponse} (also across duplicated package copies). */
|
|
3346
3442
|
function isHttpResponse(value) {
|
|
3347
|
-
|
|
3443
|
+
if (value instanceof HttpResponse) return true;
|
|
3444
|
+
if (value === null || typeof value !== "object") return false;
|
|
3445
|
+
const type = value.constructor;
|
|
3446
|
+
if (type === Object || type === Array) return false;
|
|
3447
|
+
return type?.name === "HttpResponse" && "body" in value && "headers" in value;
|
|
3348
3448
|
}
|
|
3349
3449
|
// src/serialize.ts
|
|
3350
|
-
const ENCODER = new TextEncoder();
|
|
3450
|
+
const ENCODER$1 = new TextEncoder();
|
|
3351
3451
|
/**
|
|
3352
3452
|
* UTF-8 encoding of a text body: a pooled `Buffer` (a `Uint8Array`) is several times cheaper than
|
|
3353
3453
|
* `TextEncoder` on Node.js, `TextEncoder` is the fastest on Bun.
|
|
3354
3454
|
*/
|
|
3355
|
-
const utf8 = isBun() ? (text) => ENCODER.encode(text) : (text) => Buffer$1.from(text, "utf8");
|
|
3455
|
+
const utf8$1 = isBun() ? (text) => ENCODER$1.encode(text) : (text) => Buffer$1.from(text, "utf8");
|
|
3356
3456
|
/** Content types set by the serialiser. */
|
|
3357
3457
|
const ContentType = Object.freeze({
|
|
3358
3458
|
json: "application/json; charset=utf-8",
|
|
@@ -3391,8 +3491,14 @@ function serializeResult(result, options) {
|
|
|
3391
3491
|
cookies = cookiesOf(result);
|
|
3392
3492
|
}
|
|
3393
3493
|
const finalStatus = options.status ?? status ?? defaultStatus(options.routeMethod, payload);
|
|
3394
|
-
const headers =
|
|
3395
|
-
const
|
|
3494
|
+
const headers = fastRecord();
|
|
3495
|
+
const encoded = encode(payload, headers);
|
|
3496
|
+
let body;
|
|
3497
|
+
let text;
|
|
3498
|
+
if (typeof encoded === "string") {
|
|
3499
|
+
text = encoded;
|
|
3500
|
+
body = encodeText(encoded, headers, sendsBody(finalStatus, options.method));
|
|
3501
|
+
} else body = encoded;
|
|
3396
3502
|
let own = true;
|
|
3397
3503
|
if (options.headers !== void 0) {
|
|
3398
3504
|
assignHeaders(headers, options.headers);
|
|
@@ -3406,7 +3512,7 @@ function serializeResult(result, options) {
|
|
|
3406
3512
|
assignHeaders(headers, extra);
|
|
3407
3513
|
own = false;
|
|
3408
3514
|
}
|
|
3409
|
-
return withCookies(finish(finalStatus, headers, body, options.method, own), cookies);
|
|
3515
|
+
return withCookies(finish(finalStatus, headers, body, options.method, text, own), cookies);
|
|
3410
3516
|
}
|
|
3411
3517
|
/**
|
|
3412
3518
|
* Serialises an event stream (`@Sse` result, `HttpResponse.sse()`): status 200 unless `@HttpCode`
|
|
@@ -3417,7 +3523,7 @@ function serializeResult(result, options) {
|
|
|
3417
3523
|
*/
|
|
3418
3524
|
function serializeSseReply(session, response, options) {
|
|
3419
3525
|
const status = options.status ?? response?.status ?? 200;
|
|
3420
|
-
const headers =
|
|
3526
|
+
const headers = fastRecord();
|
|
3421
3527
|
headers["cache-control"] = "no-cache, no-transform";
|
|
3422
3528
|
headers["x-accel-buffering"] = "no";
|
|
3423
3529
|
if (options.headers !== void 0) assignHeaders(headers, options.headers);
|
|
@@ -3450,7 +3556,7 @@ function withCookies(reply, cookies) {
|
|
|
3450
3556
|
*/
|
|
3451
3557
|
function serializeFileReply(file, response, options) {
|
|
3452
3558
|
const status = file.status === 200 ? options.status ?? response.status ?? 200 : file.status;
|
|
3453
|
-
const headers =
|
|
3559
|
+
const headers = fastRecord();
|
|
3454
3560
|
assignHeaders(headers, file.headers);
|
|
3455
3561
|
if (options.headers !== void 0) assignHeaders(headers, options.headers);
|
|
3456
3562
|
if (options.context !== void 0) assignHeaders(headers, options.context);
|
|
@@ -3464,8 +3570,10 @@ function defaultStatus(routeMethod, payload) {
|
|
|
3464
3570
|
}
|
|
3465
3571
|
/** Serialises an error body as JSON. */
|
|
3466
3572
|
function serializeError(status, body, method) {
|
|
3467
|
-
const headers =
|
|
3468
|
-
|
|
3573
|
+
const headers = fastRecord();
|
|
3574
|
+
const encoded = encode(body, headers);
|
|
3575
|
+
if (typeof encoded !== "string") return finish(status, headers, encoded, method);
|
|
3576
|
+
return finish(status, headers, encodeText(encoded, headers, sendsBody(status, method)), method, encoded);
|
|
3469
3577
|
}
|
|
3470
3578
|
/** Copies headers with lower-case names (later values win). */
|
|
3471
3579
|
function assignHeaders(target, source) {
|
|
@@ -3483,10 +3591,10 @@ function addMissingHeaders(target, source) {
|
|
|
3483
3591
|
}
|
|
3484
3592
|
}
|
|
3485
3593
|
/**
|
|
3486
|
-
* The reply of a body:
|
|
3487
|
-
*
|
|
3594
|
+
* The reply of a body: `text` is the string `body` encodes (see {@link encodeText}). `own`: only
|
|
3595
|
+
* {@link encode} set `headers`, which are then valid for a text body.
|
|
3488
3596
|
*/
|
|
3489
|
-
function finish(status, headers, body, method, own = false) {
|
|
3597
|
+
function finish(status, headers, body, method, text, own = false) {
|
|
3490
3598
|
if (isBodylessStatus(status)) {
|
|
3491
3599
|
discard(body);
|
|
3492
3600
|
delete headers["content-type"];
|
|
@@ -3505,7 +3613,7 @@ function finish(status, headers, body, method, own = false) {
|
|
|
3505
3613
|
body: null
|
|
3506
3614
|
};
|
|
3507
3615
|
}
|
|
3508
|
-
if (
|
|
3616
|
+
if (text === void 0) return {
|
|
3509
3617
|
status,
|
|
3510
3618
|
headers,
|
|
3511
3619
|
body
|
|
@@ -3513,22 +3621,41 @@ function finish(status, headers, body, method, own = false) {
|
|
|
3513
3621
|
return own ? {
|
|
3514
3622
|
status,
|
|
3515
3623
|
headers,
|
|
3516
|
-
body
|
|
3517
|
-
text
|
|
3624
|
+
body,
|
|
3625
|
+
text,
|
|
3518
3626
|
ownHeaders: true
|
|
3519
3627
|
} : {
|
|
3520
3628
|
status,
|
|
3521
3629
|
headers,
|
|
3522
|
-
body
|
|
3523
|
-
text
|
|
3630
|
+
body,
|
|
3631
|
+
text
|
|
3524
3632
|
};
|
|
3525
3633
|
}
|
|
3526
3634
|
function discard(body) {
|
|
3527
3635
|
if (body instanceof ReadableStream) discardStream(body);
|
|
3528
3636
|
}
|
|
3637
|
+
/** Whether a reply with this status, to this method, carries its body (see {@link finish}). */
|
|
3638
|
+
function sendsBody(status, method) {
|
|
3639
|
+
return method !== "HEAD" && !isBodylessStatus(status);
|
|
3640
|
+
}
|
|
3529
3641
|
/**
|
|
3530
|
-
*
|
|
3531
|
-
*
|
|
3642
|
+
* Sets the `content-length` of a text body (right after its `content-type`, before the route
|
|
3643
|
+
* headers) and returns its UTF-8 bytes: their length is the header, a lone surrogate counting as
|
|
3644
|
+
* U+FFFD. `null` when the reply sends no body (`HEAD`, bodyless status): nothing is encoded then,
|
|
3645
|
+
* `Buffer.byteLength` counts the same bytes.
|
|
3646
|
+
*/
|
|
3647
|
+
function encodeText(text, headers, sent) {
|
|
3648
|
+
if (!sent) {
|
|
3649
|
+
headers["content-length"] = String(Buffer$1.byteLength(text, "utf8"));
|
|
3650
|
+
return null;
|
|
3651
|
+
}
|
|
3652
|
+
const bytes = utf8$1(text);
|
|
3653
|
+
headers["content-length"] = String(bytes.byteLength);
|
|
3654
|
+
return bytes;
|
|
3655
|
+
}
|
|
3656
|
+
/**
|
|
3657
|
+
* The body of a payload, its `content-type` and, for bytes, its `content-length` set in `headers`.
|
|
3658
|
+
* Text (a string, JSON) is returned as a string: {@link encodeText} sets its length and encodes it.
|
|
3532
3659
|
*/
|
|
3533
3660
|
function encode(payload, headers) {
|
|
3534
3661
|
if (payload === void 0) return null;
|
|
@@ -3562,14 +3689,13 @@ function bytes(data, headers, type) {
|
|
|
3562
3689
|
}
|
|
3563
3690
|
function text$1(data, headers, type) {
|
|
3564
3691
|
headers["content-type"] = type;
|
|
3565
|
-
headers["content-length"] = String(Buffer$1.byteLength(data, "utf8"));
|
|
3566
3692
|
return data;
|
|
3567
3693
|
}
|
|
3568
3694
|
function isNodeReadable(value) {
|
|
3569
3695
|
return value instanceof Readable || value !== null && typeof value === "object" && typeof value.pipe === "function" && typeof value.on === "function" && typeof value.read === "function";
|
|
3570
3696
|
}
|
|
3571
3697
|
function fromWebResponse(response, method) {
|
|
3572
|
-
const headers =
|
|
3698
|
+
const headers = fastRecord();
|
|
3573
3699
|
for (const [name, value] of response.headers) if (name !== "set-cookie") headers[name] = value;
|
|
3574
3700
|
const cookies = response.headers.getSetCookie();
|
|
3575
3701
|
if (cookies.length > 0) headers["set-cookie"] = cookies;
|
|
@@ -3596,11 +3722,64 @@ const DEFAULT_SSE_SETTINGS = Object.freeze({
|
|
|
3596
3722
|
const RAW = Symbol("raw response");
|
|
3597
3723
|
const REQUEST_ID = /^[\w\-.:@/+=]{1,128}$/;
|
|
3598
3724
|
const MAX_PATH_IN_MESSAGE = 200;
|
|
3599
|
-
/** The default request id generator: a random UUID. */
|
|
3725
|
+
/** The default request id generator: a random UUID (version 4, like `crypto.randomUUID()`). */
|
|
3600
3726
|
function randomRequestId() {
|
|
3601
|
-
return
|
|
3727
|
+
return uuid();
|
|
3728
|
+
}
|
|
3729
|
+
const uuidPool = /* @__PURE__ */ new Uint8Array(2048);
|
|
3730
|
+
let uuidOffset = uuidPool.length;
|
|
3731
|
+
/** The text of the last UUID, its dashes in place. */
|
|
3732
|
+
const uuidText = Buffer$1.alloc(36, 45);
|
|
3733
|
+
/** The two lower-case hexadecimal digits of each byte, as character codes. */
|
|
3734
|
+
const HEX_CODES = /* @__PURE__ */ new Uint8Array(512);
|
|
3735
|
+
for (let byte = 0; byte < 256; byte++) {
|
|
3736
|
+
const digits = byte.toString(16).padStart(2, "0");
|
|
3737
|
+
HEX_CODES[byte * 2] = digits.charCodeAt(0);
|
|
3738
|
+
HEX_CODES[byte * 2 + 1] = digits.charCodeAt(1);
|
|
3739
|
+
}
|
|
3740
|
+
function putHex(position, byte) {
|
|
3741
|
+
uuidText[position] = HEX_CODES[byte * 2];
|
|
3742
|
+
uuidText[position + 1] = HEX_CODES[byte * 2 + 1];
|
|
3602
3743
|
}
|
|
3603
3744
|
/**
|
|
3745
|
+
* A random version 4 UUID written from a pool of random bytes (`crypto.getRandomValues`), as Node.js
|
|
3746
|
+
* does, but read as one flat string: `crypto.randomUUID()` returns a concatenation of 20 pieces that
|
|
3747
|
+
* is flattened again when the header is written, which makes it about twice as slow.
|
|
3748
|
+
*/
|
|
3749
|
+
function pooledUuid() {
|
|
3750
|
+
if (uuidOffset === uuidPool.length) {
|
|
3751
|
+
crypto.getRandomValues(uuidPool);
|
|
3752
|
+
uuidOffset = 0;
|
|
3753
|
+
}
|
|
3754
|
+
const pool = uuidPool;
|
|
3755
|
+
const at = uuidOffset;
|
|
3756
|
+
uuidOffset = at + 16;
|
|
3757
|
+
putHex(0, pool[at]);
|
|
3758
|
+
putHex(2, pool[at + 1]);
|
|
3759
|
+
putHex(4, pool[at + 2]);
|
|
3760
|
+
putHex(6, pool[at + 3]);
|
|
3761
|
+
putHex(9, pool[at + 4]);
|
|
3762
|
+
putHex(11, pool[at + 5]);
|
|
3763
|
+
putHex(14, pool[at + 6] & 15 | 64);
|
|
3764
|
+
putHex(16, pool[at + 7]);
|
|
3765
|
+
putHex(19, pool[at + 8] & 63 | 128);
|
|
3766
|
+
putHex(21, pool[at + 9]);
|
|
3767
|
+
putHex(24, pool[at + 10]);
|
|
3768
|
+
putHex(26, pool[at + 11]);
|
|
3769
|
+
putHex(28, pool[at + 12]);
|
|
3770
|
+
putHex(30, pool[at + 13]);
|
|
3771
|
+
putHex(32, pool[at + 14]);
|
|
3772
|
+
putHex(34, pool[at + 15]);
|
|
3773
|
+
return readUuidText();
|
|
3774
|
+
}
|
|
3775
|
+
/**
|
|
3776
|
+
* The text of {@link uuidText}: Node.js `Buffer#latin1Slice` (what `toString('latin1')` calls after
|
|
3777
|
+
* checking its arguments) when present.
|
|
3778
|
+
*/
|
|
3779
|
+
const readUuidText = typeof uuidText.latin1Slice === "function" ? () => uuidText.latin1Slice(0, 36) : () => uuidText.toString("latin1", 0, 36);
|
|
3780
|
+
/** UUID generator: Bun's native `crypto.randomUUID()` is the fastest there. */
|
|
3781
|
+
const uuid = isBun() ? () => crypto.randomUUID() : pooledUuid;
|
|
3782
|
+
/**
|
|
3604
3783
|
* The request pipeline (architecture §9.1):
|
|
3605
3784
|
* `runInScope({ HttpContext, RequestId }) → hooks.run('http:request') → guards → body → interceptors
|
|
3606
3785
|
* → invoke → serialise`, errors being mapped to responses inside `hooks.run`.
|
|
@@ -3615,6 +3794,22 @@ var Pipeline = class {
|
|
|
3615
3794
|
#securityHeaders;
|
|
3616
3795
|
/** Every request id can be sent: an incoming one matched `REQUEST_ID`, the generated ones are UUIDs. */
|
|
3617
3796
|
#validIds;
|
|
3797
|
+
/**
|
|
3798
|
+
* The headers of a JSON reply of the fast lane, in their final order (content type and length,
|
|
3799
|
+
* then the framework headers, see {@link #frameworkHeaders}): copied, then completed with the
|
|
3800
|
+
* length and the request id.
|
|
3801
|
+
*/
|
|
3802
|
+
#jsonHeaders;
|
|
3803
|
+
/** The request id header of {@link #jsonHeaders}, `undefined` when the reply has none. */
|
|
3804
|
+
#jsonIdHeader;
|
|
3805
|
+
/** The headers of a reply without body of the fast lane (the framework headers, see {@link #jsonHeaders}). */
|
|
3806
|
+
#emptyHeaders;
|
|
3807
|
+
/** The request id header of {@link #emptyHeaders}, `undefined` when the reply has none. */
|
|
3808
|
+
#emptyIdHeader;
|
|
3809
|
+
/** Whether {@link #jsonHeaders} and {@link #emptyHeaders} hold every framework header (see {@link #fastRoute}). */
|
|
3810
|
+
#fastHeaders;
|
|
3811
|
+
/** The `cookies` option (secure defaults, no signing, when not set). */
|
|
3812
|
+
#cookies;
|
|
3618
3813
|
#inflight = 0;
|
|
3619
3814
|
#idle = [];
|
|
3620
3815
|
/** Open event streams. */
|
|
@@ -3628,7 +3823,22 @@ var Pipeline = class {
|
|
|
3628
3823
|
this.#options = options;
|
|
3629
3824
|
this.#table = table;
|
|
3630
3825
|
this.#validIds = options.generateRequestId === randomRequestId;
|
|
3826
|
+
this.#cookies = options.cookies ?? DEFAULT_COOKIE_SETTINGS;
|
|
3631
3827
|
this.#securityHeaders = Object.entries(options.securityHeaders ?? {}).map(([name, value]) => [name.toLowerCase(), value]);
|
|
3828
|
+
const placeholder = "-";
|
|
3829
|
+
const idHeader = options.requestIdHeader;
|
|
3830
|
+
const json = {
|
|
3831
|
+
"content-type": ContentType.json,
|
|
3832
|
+
"content-length": "0"
|
|
3833
|
+
};
|
|
3834
|
+
this.#frameworkHeaders(json, placeholder, void 0, false);
|
|
3835
|
+
this.#jsonHeaders = json;
|
|
3836
|
+
this.#jsonIdHeader = idHeader !== void 0 && json[idHeader] === placeholder ? idHeader : void 0;
|
|
3837
|
+
const empty = {};
|
|
3838
|
+
this.#frameworkHeaders(empty, placeholder, void 0, false);
|
|
3839
|
+
this.#emptyHeaders = empty;
|
|
3840
|
+
this.#emptyIdHeader = idHeader !== void 0 && empty[idHeader] === placeholder ? idHeader : void 0;
|
|
3841
|
+
this.#fastHeaders = (idHeader === void 0 || !(idHeader in Object.prototype)) && this.#securityHeaders.every(([name]) => !(name in Object.prototype));
|
|
3632
3842
|
}
|
|
3633
3843
|
/** Number of requests being processed. */
|
|
3634
3844
|
get inflight() {
|
|
@@ -3651,6 +3861,349 @@ var Pipeline = class {
|
|
|
3651
3861
|
return new Promise((resolve) => this.#idle.push(resolve));
|
|
3652
3862
|
}
|
|
3653
3863
|
/**
|
|
3864
|
+
* The adapter handler of a route: {@link handle}, or a fast lane for the routes that need no
|
|
3865
|
+
* guard, interceptor, event stream, raw response nor `@Header` when CORS is off (see
|
|
3866
|
+
* {@link #fast}).
|
|
3867
|
+
*/
|
|
3868
|
+
handler(plan) {
|
|
3869
|
+
const route = this.#fastRoute(plan);
|
|
3870
|
+
if (route === void 0) return (request) => this.handle(plan, request);
|
|
3871
|
+
return (request) => this.#fast(route, request);
|
|
3872
|
+
}
|
|
3873
|
+
/** The fast lane of a route, `undefined` when the route needs the general pipeline. */
|
|
3874
|
+
#fastRoute(plan) {
|
|
3875
|
+
const definition = plan.definition;
|
|
3876
|
+
if (!this.#fastHeaders || this.#options.cors !== void 0 || plan.redirect === true || plan.guards.length > 0 || plan.interceptors.length > 0 || definition.sse !== void 0 || definition.rawResponse === true || definition.headers !== void 0 || definition.method === "ALL") return;
|
|
3877
|
+
const post = definition.method === "POST";
|
|
3878
|
+
const json = definition.status ?? (post ? 201 : 200);
|
|
3879
|
+
return {
|
|
3880
|
+
plan,
|
|
3881
|
+
target: {
|
|
3882
|
+
kind: "route",
|
|
3883
|
+
plan
|
|
3884
|
+
},
|
|
3885
|
+
method: definition.method,
|
|
3886
|
+
jsonStatus: definition.method === "HEAD" || isBodylessStatus(json) ? void 0 : json,
|
|
3887
|
+
emptyStatus: definition.status ?? (post ? 201 : 204)
|
|
3888
|
+
};
|
|
3889
|
+
}
|
|
3890
|
+
/**
|
|
3891
|
+
* The fast lane: what {@link handle} does for a route of {@link #fastRoute}, decided once instead
|
|
3892
|
+
* of per request, without promises while the route stays synchronous. A request takes it when the
|
|
3893
|
+
* server is not draining and its method is the route's (not a `HEAD` served by a `GET` route); a
|
|
3894
|
+
* plain object or array returned by the handler is serialised as JSON, and `undefined` answered
|
|
3895
|
+
* without body, with the headers in their final order (a template precomputed at boot).
|
|
3896
|
+
* A thenable result is awaited in the lane (see {@link #fastAsync}). Anything else — a malformed
|
|
3897
|
+
* path (before the request starts), a thrown error or a rejection, an `HttpResponse`, a `Response`
|
|
3898
|
+
* or any other result, headers or cookies set through the context — continues in the general
|
|
3899
|
+
* pipeline, in the same request scope, with the same outcome.
|
|
3900
|
+
* With `http:request` wrappers, the handler runs inside them (see {@link #fastWrapped}); listeners
|
|
3901
|
+
* get the event once the reply is ready, as in the general pipeline.
|
|
3902
|
+
*/
|
|
3903
|
+
#fast(route, request) {
|
|
3904
|
+
const plan = route.plan;
|
|
3905
|
+
if (this.draining || request.method !== route.method) return this.handle(plan, request);
|
|
3906
|
+
const hooks = this.#hooks;
|
|
3907
|
+
const wrapped = hooks.hasWrappers("http:request");
|
|
3908
|
+
const startTime = wrapped || hooks.hasListeners("http:request") ? performance.now() : void 0;
|
|
3909
|
+
const url = request.url;
|
|
3910
|
+
const mark = url.indexOf("?");
|
|
3911
|
+
const path = mark === -1 ? url : url.slice(0, mark);
|
|
3912
|
+
const params = routeParams(plan.path, path);
|
|
3913
|
+
if (params === void 0) return this.handle(plan, request);
|
|
3914
|
+
this.#enter();
|
|
3915
|
+
let ctx;
|
|
3916
|
+
try {
|
|
3917
|
+
const query = mark === -1 ? "" : url.slice(mark + 1);
|
|
3918
|
+
ctx = this.#context(request, this.#requestId(request), path, query, plan.info, params, plan.body);
|
|
3919
|
+
} catch (error) {
|
|
3920
|
+
this.#leave();
|
|
3921
|
+
return Promise.reject(error);
|
|
3922
|
+
}
|
|
3923
|
+
const scope = this.#container.openScope([[HttpTokens.Context, ctx], [HttpTokens.RequestId, ctx.requestId]]);
|
|
3924
|
+
if (wrapped) {
|
|
3925
|
+
const x = new Exchange(request, ctx, ctx.requestId, route.target, startTime);
|
|
3926
|
+
x.done = scope.run(this.#fastWrapped, x, route);
|
|
3927
|
+
return this.#fastExchange(x, scope);
|
|
3928
|
+
}
|
|
3929
|
+
const done = scope.run(this.#fastRun, route, request, ctx, startTime);
|
|
3930
|
+
if (done instanceof Exchange) return this.#fastExchange(done, scope);
|
|
3931
|
+
const closing = scope.close();
|
|
3932
|
+
if (closing === void 0) {
|
|
3933
|
+
ctx.complete();
|
|
3934
|
+
this.#leave();
|
|
3935
|
+
} else {
|
|
3936
|
+
const x = new Exchange(request, ctx, ctx.requestId, route.target);
|
|
3937
|
+
x.answer(done);
|
|
3938
|
+
closing.then(() => this.#over(x), (error) => this.#disposalFailed(x, error));
|
|
3939
|
+
}
|
|
3940
|
+
if (!hooks.hasListeners("http:request")) return Promise.resolve(done);
|
|
3941
|
+
const event = requestEvent(request, ctx, startTime ?? performance.now());
|
|
3942
|
+
event.status = done.status;
|
|
3943
|
+
return Promise.resolve(done).then((reply) => {
|
|
3944
|
+
hooks.emit("http:request", event);
|
|
3945
|
+
return reply;
|
|
3946
|
+
});
|
|
3947
|
+
}
|
|
3948
|
+
/**
|
|
3949
|
+
* The reply of a fast-lane request whose asynchronous part runs in `x.done`: the request ends once
|
|
3950
|
+
* it settled (see {@link #end}); listeners get the event once the reply is ready.
|
|
3951
|
+
*/
|
|
3952
|
+
#fastExchange(x, scope) {
|
|
3953
|
+
x.done.then(() => this.#end(x, scope, void 0), (error) => this.#end(x, scope, error));
|
|
3954
|
+
if (!this.#hooks.hasListeners("http:request")) return x.replied;
|
|
3955
|
+
x.tracked = true;
|
|
3956
|
+
return this.#emitted(x);
|
|
3957
|
+
}
|
|
3958
|
+
/**
|
|
3959
|
+
* The fast lane in the request scope: the reply when the handler answered synchronously with a
|
|
3960
|
+
* plain JSON value or `undefined`, otherwise the exchange finishing the request (see {@link #fast}).
|
|
3961
|
+
* `startTime`: the request has `http:request` listeners, its event starts with the exchange.
|
|
3962
|
+
*/
|
|
3963
|
+
#fastRun = (route, request, ctx, startTime) => {
|
|
3964
|
+
const plan = route.plan;
|
|
3965
|
+
if (plan.parseBody) return this.#fastLater(route, request, ctx, NO_RESULT, startTime);
|
|
3966
|
+
let result;
|
|
3967
|
+
let reply;
|
|
3968
|
+
try {
|
|
3969
|
+
result = plan.definition.invoke(this.#resolve(plan.controller), ctx);
|
|
3970
|
+
if (isThenable(result)) return this.#fastLater(route, request, ctx, result, startTime);
|
|
3971
|
+
reply = this.#fastReply(route, ctx, result);
|
|
3972
|
+
} catch (error) {
|
|
3973
|
+
const x = new Exchange(request, ctx, ctx.requestId, route.target, startTime);
|
|
3974
|
+
x.done = this.#respond(x, () => this.#failed(error, ctx, x.event));
|
|
3975
|
+
return x;
|
|
3976
|
+
}
|
|
3977
|
+
if (reply !== void 0) {
|
|
3978
|
+
ctx.commit();
|
|
3979
|
+
ctx.releaseBody();
|
|
3980
|
+
return reply;
|
|
3981
|
+
}
|
|
3982
|
+
const x = new Exchange(request, ctx, ctx.requestId, route.target, startTime);
|
|
3983
|
+
x.done = this.#respond(x, () => this.#settle(ctx, plan, x.event, result));
|
|
3984
|
+
return x;
|
|
3985
|
+
};
|
|
3986
|
+
/** The fast lane of a route with a body to read or a handler that returned a thenable. */
|
|
3987
|
+
#fastLater(route, request, ctx, result, startTime) {
|
|
3988
|
+
const x = new Exchange(request, ctx, ctx.requestId, route.target, startTime);
|
|
3989
|
+
x.done = this.#fastAsync(x, route, result);
|
|
3990
|
+
return x;
|
|
3991
|
+
}
|
|
3992
|
+
/**
|
|
3993
|
+
* The asynchronous part of the fast lane, in the request scope: the body read (`result` is
|
|
3994
|
+
* {@link NO_RESULT}) and the handler run, or the thenable it returned awaited. Never rejects.
|
|
3995
|
+
*/
|
|
3996
|
+
async #fastAsync(x, route, pending) {
|
|
3997
|
+
const ctx = x.ctx;
|
|
3998
|
+
const plan = route.plan;
|
|
3999
|
+
let result = pending;
|
|
4000
|
+
let reply;
|
|
4001
|
+
try {
|
|
4002
|
+
if (result === NO_RESULT) {
|
|
4003
|
+
const controller = this.#resolve(plan.controller);
|
|
4004
|
+
await ctx.parseBody();
|
|
4005
|
+
result = plan.definition.invoke(controller, ctx);
|
|
4006
|
+
}
|
|
4007
|
+
if (isThenable(result)) result = await result;
|
|
4008
|
+
reply = this.#fastReply(route, ctx, result);
|
|
4009
|
+
} catch (error) {
|
|
4010
|
+
await this.#respond(x, () => this.#failed(error, ctx, x.event));
|
|
4011
|
+
return;
|
|
4012
|
+
}
|
|
4013
|
+
if (reply === void 0) {
|
|
4014
|
+
const value = result;
|
|
4015
|
+
await this.#respond(x, () => this.#settle(ctx, plan, x.event, value));
|
|
4016
|
+
return;
|
|
4017
|
+
}
|
|
4018
|
+
ctx.commit();
|
|
4019
|
+
ctx.releaseBody();
|
|
4020
|
+
if (x.tracked) x.event.status = reply.status;
|
|
4021
|
+
x.answer(reply);
|
|
4022
|
+
}
|
|
4023
|
+
/**
|
|
4024
|
+
* The reply of a plain JSON value (see {@link isPlainJson}) or of `undefined`: what
|
|
4025
|
+
* `serializeResult` and {@link #complete} produce for it, the headers in their final order.
|
|
4026
|
+
* `undefined` when the result or the context needs the general pipeline. Throws like
|
|
4027
|
+
* `JSON.stringify`.
|
|
4028
|
+
*/
|
|
4029
|
+
#fastReply(route, ctx, result) {
|
|
4030
|
+
const status = result === void 0 ? route.emptyStatus : route.jsonStatus;
|
|
4031
|
+
if (status === void 0 || ctx.hasResponseHeaders || ctx.hasCookies || this.draining) return void 0;
|
|
4032
|
+
const requestId = ctx.requestId;
|
|
4033
|
+
if (!this.#validIds && !isHeaderValue(requestId)) return void 0;
|
|
4034
|
+
if (result === void 0) return this.#templateReply(status, null, void 0, requestId);
|
|
4035
|
+
if (!isPlainJson(result)) return void 0;
|
|
4036
|
+
const json = JSON.stringify(result);
|
|
4037
|
+
if (json === void 0) throw new TypeError(`Cannot serialise a ${typeof result} as a response body`);
|
|
4038
|
+
return this.#templateReply(status, utf8(json), json, requestId);
|
|
4039
|
+
}
|
|
4040
|
+
/**
|
|
4041
|
+
* The reply of a JSON body (`text` set) or of no body, its headers copied from the template of
|
|
4042
|
+
* the fast lane (see {@link #jsonHeaders}), completed with the length and the request id (one that
|
|
4043
|
+
* can be sent).
|
|
4044
|
+
*/
|
|
4045
|
+
#templateReply(status, body, text, requestId) {
|
|
4046
|
+
if (text === void 0) {
|
|
4047
|
+
const headers = { ...this.#emptyHeaders };
|
|
4048
|
+
const idHeader = this.#emptyIdHeader;
|
|
4049
|
+
if (idHeader !== void 0) headers[idHeader] = requestId;
|
|
4050
|
+
return {
|
|
4051
|
+
status,
|
|
4052
|
+
headers,
|
|
4053
|
+
body: null
|
|
4054
|
+
};
|
|
4055
|
+
}
|
|
4056
|
+
const headers = { ...this.#jsonHeaders };
|
|
4057
|
+
headers["content-length"] = String(body.byteLength);
|
|
4058
|
+
const idHeader = this.#jsonIdHeader;
|
|
4059
|
+
if (idHeader !== void 0) headers[idHeader] = requestId;
|
|
4060
|
+
return {
|
|
4061
|
+
status,
|
|
4062
|
+
headers,
|
|
4063
|
+
body,
|
|
4064
|
+
text
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
4067
|
+
/**
|
|
4068
|
+
* The fast lane through the `http:request` wrappers (registered when the request started), in the
|
|
4069
|
+
* request scope: what {@link #respond} does for the route, the handler run inside `next()` like
|
|
4070
|
+
* {@link #fastRun}. A JSON value or `undefined` is serialised there as `serializeResult` does, and
|
|
4071
|
+
* the reply completed from the template of the fast lane when the wrappers added no response header
|
|
4072
|
+
* (`event.responseHeaders`) nor cookie (see {@link #fastDeliver}). Never rejects.
|
|
4073
|
+
*/
|
|
4074
|
+
#fastWrapped = (x, route) => this.#hooks.run("http:request", x.event, () => this.#fastOperation(x, route)).then(() => this.#fastDeliver(x, false, void 0), (error) => this.#fastDeliver(x, true, error));
|
|
4075
|
+
/** The operation wrapped by the `http:request` wrappers in the fast lane: the route run, its outcome checked. */
|
|
4076
|
+
#fastOperation(x, route) {
|
|
4077
|
+
const ctx = x.ctx;
|
|
4078
|
+
const plan = route.plan;
|
|
4079
|
+
if (plan.parseBody) return this.#fastOperationLater(x, route, NO_RESULT);
|
|
4080
|
+
let result;
|
|
4081
|
+
try {
|
|
4082
|
+
result = plan.definition.invoke(this.#resolve(plan.controller), ctx);
|
|
4083
|
+
} catch (error) {
|
|
4084
|
+
return this.#fastChecked(x, this.#failed(error, ctx, x.event));
|
|
4085
|
+
}
|
|
4086
|
+
if (isThenable(result)) return this.#fastOperationLater(x, route, result);
|
|
4087
|
+
return this.#fastChecked(x, this.#fastOutcome(x, route, result));
|
|
4088
|
+
}
|
|
4089
|
+
/** {@link #fastOperation} with a body to read (`pending` is {@link NO_RESULT}) or a thenable result. */
|
|
4090
|
+
async #fastOperationLater(x, route, pending) {
|
|
4091
|
+
const ctx = x.ctx;
|
|
4092
|
+
const plan = route.plan;
|
|
4093
|
+
let result = pending;
|
|
4094
|
+
try {
|
|
4095
|
+
if (result === NO_RESULT) {
|
|
4096
|
+
const controller = this.#resolve(plan.controller);
|
|
4097
|
+
await ctx.parseBody();
|
|
4098
|
+
result = plan.definition.invoke(controller, ctx);
|
|
4099
|
+
}
|
|
4100
|
+
if (isThenable(result)) result = await result;
|
|
4101
|
+
} catch (error) {
|
|
4102
|
+
return await this.#fastChecked(x, this.#failed(error, ctx, x.event));
|
|
4103
|
+
}
|
|
4104
|
+
return await this.#fastChecked(x, this.#fastOutcome(x, route, result));
|
|
4105
|
+
}
|
|
4106
|
+
/**
|
|
4107
|
+
* The outcome of a handler result in {@link #fastOperation}: for a plain JSON value or `undefined`,
|
|
4108
|
+
* the reply of `serializeResult` built directly (`x.fast` set); anything else as in the general
|
|
4109
|
+
* pipeline (see {@link #settle}). Never throws nor rejects (errors are mapped).
|
|
4110
|
+
*/
|
|
4111
|
+
#fastOutcome(x, route, result) {
|
|
4112
|
+
const ctx = x.ctx;
|
|
4113
|
+
const event = x.event;
|
|
4114
|
+
const status = result === void 0 ? route.emptyStatus : route.jsonStatus;
|
|
4115
|
+
if (status === void 0 || ctx.hasResponseHeaders || result !== void 0 && !isPlainJson(result)) return this.#settle(ctx, route.plan, event, result);
|
|
4116
|
+
let reply;
|
|
4117
|
+
if (result === void 0) reply = {
|
|
4118
|
+
status,
|
|
4119
|
+
headers: fastRecord(),
|
|
4120
|
+
body: null
|
|
4121
|
+
};
|
|
4122
|
+
else {
|
|
4123
|
+
let json;
|
|
4124
|
+
try {
|
|
4125
|
+
json = JSON.stringify(result);
|
|
4126
|
+
if (json === void 0) throw new TypeError(`Cannot serialise a ${typeof result} as a response body`);
|
|
4127
|
+
} catch (error) {
|
|
4128
|
+
return this.#failed(error, ctx, event);
|
|
4129
|
+
}
|
|
4130
|
+
const headers = fastRecord();
|
|
4131
|
+
headers["content-type"] = ContentType.json;
|
|
4132
|
+
const body = utf8(json);
|
|
4133
|
+
headers["content-length"] = String(body.byteLength);
|
|
4134
|
+
reply = {
|
|
4135
|
+
status,
|
|
4136
|
+
headers,
|
|
4137
|
+
body,
|
|
4138
|
+
text: json,
|
|
4139
|
+
ownHeaders: true
|
|
4140
|
+
};
|
|
4141
|
+
}
|
|
4142
|
+
event.status = status;
|
|
4143
|
+
x.fast = true;
|
|
4144
|
+
return reply;
|
|
4145
|
+
}
|
|
4146
|
+
/**
|
|
4147
|
+
* What the general pipeline does with an outcome inside the wrappers (see {@link #respond}): its
|
|
4148
|
+
* headers and cookies checked, the async context of a streamed body captured; kept in `x.outcome`.
|
|
4149
|
+
*/
|
|
4150
|
+
#fastChecked(x, pending) {
|
|
4151
|
+
if (pending instanceof Promise) return pending.then((outcome) => this.#fastKeep(x, outcome));
|
|
4152
|
+
return Promise.resolve(this.#fastKeep(x, pending));
|
|
4153
|
+
}
|
|
4154
|
+
#fastKeep(x, outcome) {
|
|
4155
|
+
const checked = x.fast ? outcome : this.#checked(outcome, x.ctx, x.event);
|
|
4156
|
+
if (checked !== RAW && isStreamed(checked.body)) x.run = captureContext();
|
|
4157
|
+
x.outcome = checked;
|
|
4158
|
+
return checked;
|
|
4159
|
+
}
|
|
4160
|
+
/**
|
|
4161
|
+
* Answers a fast-lane request once its wrappers settled: a reply of {@link #fastOutcome} gets the
|
|
4162
|
+
* headers of the template when nothing was added to it (no response header from the wrappers, no
|
|
4163
|
+
* cookie, not draining, a request id that can be sent), which is what {@link #complete} gives;
|
|
4164
|
+
* anything else is completed by {@link #deliver}. Never rejects.
|
|
4165
|
+
*/
|
|
4166
|
+
#fastDeliver(x, failed, hookError) {
|
|
4167
|
+
const outcome = x.outcome;
|
|
4168
|
+
const ctx = x.ctx;
|
|
4169
|
+
if (outcome === void 0) return this.#fastUnhandled(x, failed ? hookError : NOT_CALLED);
|
|
4170
|
+
if (failed) this.#wrapperFailed(x, hookError);
|
|
4171
|
+
else if (x.fast && outcome !== RAW && !hasOwnKeys(x.event.responseHeaders) && !ctx.hasCookies && !this.draining && (this.#validIds || isHeaderValue(x.requestId))) {
|
|
4172
|
+
const reply = this.#templateReply(outcome.status, outcome.body, outcome.text, x.requestId);
|
|
4173
|
+
ctx.commit();
|
|
4174
|
+
ctx.releaseBody();
|
|
4175
|
+
x.answer(reply);
|
|
4176
|
+
return;
|
|
4177
|
+
}
|
|
4178
|
+
const streaming = this.#send(x, outcome, true, x.run);
|
|
4179
|
+
if (streaming === void 0) return void 0;
|
|
4180
|
+
return streaming.tracked.done.then((failure) => this.#streamed(x, streaming, failure));
|
|
4181
|
+
}
|
|
4182
|
+
/** {@link #fastDeliver} when a wrapper failed before the request was handled or did not call `next()`. */
|
|
4183
|
+
async #fastUnhandled(x, failure) {
|
|
4184
|
+
const streaming = this.#send(x, await this.#unhandled(x, failure), true, void 0);
|
|
4185
|
+
if (streaming !== void 0) this.#streamed(x, streaming, await streaming.tracked.done);
|
|
4186
|
+
}
|
|
4187
|
+
/** Ends a fast-lane request once its asynchronous part settled: its scope is closed, then it is over. */
|
|
4188
|
+
#end(x, scope, error) {
|
|
4189
|
+
const closing = scope.close();
|
|
4190
|
+
if (error !== void 0) {
|
|
4191
|
+
const report = () => this.#disposalFailed(x, error);
|
|
4192
|
+
if (closing === void 0) report();
|
|
4193
|
+
else closing.then(report, report);
|
|
4194
|
+
return;
|
|
4195
|
+
}
|
|
4196
|
+
if (closing === void 0) this.#over(x);
|
|
4197
|
+
else closing.then(() => this.#over(x), (failure) => this.#disposalFailed(x, failure));
|
|
4198
|
+
}
|
|
4199
|
+
#disposalFailed(x, error) {
|
|
4200
|
+
this.#over(x);
|
|
4201
|
+
this.#logger.error("Failed to dispose request-scoped providers", {
|
|
4202
|
+
err: error,
|
|
4203
|
+
requestId: x.requestId
|
|
4204
|
+
});
|
|
4205
|
+
}
|
|
4206
|
+
/**
|
|
3654
4207
|
* Handles a request routed to `plan` by the adapter. The request stays in flight (and its scope
|
|
3655
4208
|
* open) until a streamed or file response body has been fully read or cancelled by the adapter.
|
|
3656
4209
|
*/
|
|
@@ -3746,146 +4299,172 @@ var Pipeline = class {
|
|
|
3746
4299
|
}
|
|
3747
4300
|
#serve(request, url, route, params, target, startTime, body) {
|
|
3748
4301
|
this.#enter();
|
|
3749
|
-
let
|
|
3750
|
-
let ctx;
|
|
3751
|
-
let event;
|
|
4302
|
+
let x;
|
|
3752
4303
|
try {
|
|
3753
|
-
requestId = this.#requestId(request);
|
|
3754
|
-
const
|
|
3755
|
-
ctx = new RequestContext(request, {
|
|
3756
|
-
requestId,
|
|
3757
|
-
adapter: this.#options.adapter,
|
|
3758
|
-
url,
|
|
3759
|
-
route,
|
|
3760
|
-
params,
|
|
3761
|
-
body,
|
|
3762
|
-
cookies: this.#options.cookies ?? DEFAULT_COOKIE_SETTINGS,
|
|
3763
|
-
client: proxy === void 0 ? void 0 : proxy.resolve(request.remoteAddress ?? request.ip, request.headers, request.encrypted === true)
|
|
3764
|
-
});
|
|
4304
|
+
const requestId = this.#requestId(request);
|
|
4305
|
+
const ctx = this.#context(request, requestId, url.path, url.query, route, params, body);
|
|
3765
4306
|
if (this.draining && target.kind === "route" && target.plan.definition.sse !== void 0) ctx.abort("shutdown");
|
|
3766
|
-
|
|
3767
|
-
method: request.method,
|
|
3768
|
-
path: ctx.path,
|
|
3769
|
-
route: route?.path,
|
|
3770
|
-
requestId,
|
|
3771
|
-
headers: request.headers,
|
|
3772
|
-
context: ctx,
|
|
3773
|
-
startTime,
|
|
3774
|
-
responseHeaders: {}
|
|
3775
|
-
};
|
|
4307
|
+
x = new Exchange(request, ctx, requestId, target, startTime);
|
|
3776
4308
|
} catch (error) {
|
|
3777
4309
|
this.#leave();
|
|
3778
4310
|
return Promise.reject(error);
|
|
3779
4311
|
}
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
resolve = onReply;
|
|
3787
|
-
reject = onError;
|
|
4312
|
+
this.#container.runInScope(() => this.#respond(x, void 0), [[HttpTokens.Context, x.ctx], [HttpTokens.RequestId, x.requestId]]).then(() => this.#over(x), (error) => {
|
|
4313
|
+
this.#over(x);
|
|
4314
|
+
this.#logger.error("Failed to dispose request-scoped providers", {
|
|
4315
|
+
err: error,
|
|
4316
|
+
requestId: x.requestId
|
|
4317
|
+
});
|
|
3788
4318
|
});
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
4319
|
+
return this.#hooks.hasListeners("http:request") ? this.#emitted(x) : x.replied;
|
|
4320
|
+
}
|
|
4321
|
+
/** The reply of a request once the `http:request` listeners saw its event. */
|
|
4322
|
+
#emitted(x) {
|
|
4323
|
+
return x.replied.then((reply) => {
|
|
4324
|
+
this.#hooks.emit("http:request", x.event);
|
|
4325
|
+
return reply;
|
|
4326
|
+
});
|
|
4327
|
+
}
|
|
4328
|
+
/** The request context of a request (internal). */
|
|
4329
|
+
#context(request, requestId, path, query, route, params, body) {
|
|
4330
|
+
const proxy = this.#options.trustProxy;
|
|
4331
|
+
return new RequestContext(request, requestId, this.#options.adapter, path, query, route, params, body, this.#cookies, proxy === void 0 ? void 0 : proxy.resolve(request.remoteAddress ?? request.ip, request.headers, request.encrypted === true));
|
|
4332
|
+
}
|
|
4333
|
+
/**
|
|
4334
|
+
* Answers a request in its scope: its outcome (`process`, or the target of the exchange after the
|
|
4335
|
+
* CORS decision and through the `http:request` wrappers), checked, completed and handed to the
|
|
4336
|
+
* adapter; a streamed body is then tracked until it was sent or abandoned. Never rejects.
|
|
4337
|
+
* `process` is given by the fast lane, which decided at the start of the request that no wrapper
|
|
4338
|
+
* applies and that CORS is off.
|
|
4339
|
+
*/
|
|
4340
|
+
async #respond(x, process) {
|
|
4341
|
+
const ctx = x.ctx;
|
|
4342
|
+
const requestId = x.requestId;
|
|
4343
|
+
const event = x.event;
|
|
4344
|
+
let outcome;
|
|
4345
|
+
let hookError;
|
|
4346
|
+
let failed = false;
|
|
4347
|
+
let run;
|
|
4348
|
+
let recheck = false;
|
|
4349
|
+
let work = process;
|
|
4350
|
+
if (work === void 0) {
|
|
4351
|
+
if (this.#options.cors !== void 0) x.cors = await this.#corsDecision(x.request, requestId);
|
|
4352
|
+
recheck = this.#hooks.hasWrappers("http:request");
|
|
4353
|
+
const target = x.target;
|
|
4354
|
+
work = () => this.#process(ctx, target, event, x.cors);
|
|
4355
|
+
}
|
|
4356
|
+
try {
|
|
4357
|
+
if (recheck) {
|
|
4358
|
+
const inner = work;
|
|
4359
|
+
await this.#hooks.run("http:request", event, async () => {
|
|
4360
|
+
outcome = this.#checked(await inner(), ctx, event);
|
|
3820
4361
|
if (outcome !== RAW && isStreamed(outcome.body)) run = captureContext();
|
|
3821
4362
|
return outcome;
|
|
3822
4363
|
});
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
if (this.draining) session.close("shutdown");
|
|
3866
|
-
}
|
|
4364
|
+
} else outcome = this.#checked(await work(), ctx, event);
|
|
4365
|
+
} catch (error) {
|
|
4366
|
+
failed = true;
|
|
4367
|
+
hookError = error;
|
|
4368
|
+
}
|
|
4369
|
+
if (outcome === void 0) {
|
|
4370
|
+
outcome = await this.#unhandled(x, failed ? hookError : NOT_CALLED);
|
|
4371
|
+
recheck = true;
|
|
4372
|
+
} else if (failed) this.#wrapperFailed(x, hookError);
|
|
4373
|
+
const streaming = this.#send(x, outcome, recheck, run);
|
|
4374
|
+
if (streaming !== void 0) this.#streamed(x, streaming, await streaming.tracked.done);
|
|
4375
|
+
}
|
|
4376
|
+
/**
|
|
4377
|
+
* The reply of a request whose `http:request` wrapper failed before it was handled (`error`), or
|
|
4378
|
+
* did not call `next()` ({@link NOT_CALLED}): the error mapped and checked like a handled reply (an
|
|
4379
|
+
* exception mapper's `HttpResponse` may carry cookies). Never rejects.
|
|
4380
|
+
*/
|
|
4381
|
+
async #unhandled(x, failure) {
|
|
4382
|
+
const ctx = x.ctx;
|
|
4383
|
+
const event = x.event;
|
|
4384
|
+
const error = failure === NOT_CALLED ? /* @__PURE__ */ new Error("An \"http:request\" hook wrapper did not call next()") : failure;
|
|
4385
|
+
event.error ??= error;
|
|
4386
|
+
const fallback = await this.#errorReply(error, ctx);
|
|
4387
|
+
const reply = this.#checkCookies(this.#checkHeaders(fallback, ctx, event), ctx, event);
|
|
4388
|
+
event.status = reply.status;
|
|
4389
|
+
return reply;
|
|
4390
|
+
}
|
|
4391
|
+
#wrapperFailed(x, error) {
|
|
4392
|
+
this.#logger.error("An \"http:request\" hook wrapper failed after the request was handled", {
|
|
4393
|
+
err: error,
|
|
4394
|
+
requestId: x.requestId
|
|
4395
|
+
});
|
|
4396
|
+
}
|
|
4397
|
+
/**
|
|
4398
|
+
* Completes the outcome of a request (`recheck`: wrappers ran) and hands its reply to the adapter.
|
|
4399
|
+
* Returns the tracking of a streamed body (read in `run`, the context of the handler, when it was
|
|
4400
|
+
* captured), whose end {@link #streamed} records.
|
|
4401
|
+
*/
|
|
4402
|
+
#send(x, outcome, recheck, run) {
|
|
4403
|
+
const ctx = x.ctx;
|
|
4404
|
+
const event = x.event;
|
|
4405
|
+
if (outcome === RAW) {
|
|
3867
4406
|
ctx.commit();
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
4407
|
+
const status = x.request.rawStatus?.() ?? 200;
|
|
4408
|
+
event.status ??= status;
|
|
4409
|
+
x.answer({
|
|
4410
|
+
status,
|
|
4411
|
+
headers: {},
|
|
4412
|
+
body: null,
|
|
4413
|
+
raw: true
|
|
3875
4414
|
});
|
|
4415
|
+
return;
|
|
4416
|
+
}
|
|
4417
|
+
const serialized = this.#complete(outcome, ctx, event, x.requestId, x.cors, x.preflight, recheck);
|
|
4418
|
+
const body = serialized.body;
|
|
4419
|
+
if (!isStreamed(body)) {
|
|
4420
|
+
const reply = adapterReply(serialized, body);
|
|
4421
|
+
ctx.commit();
|
|
3876
4422
|
ctx.releaseBody();
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
4423
|
+
x.answer(reply);
|
|
4424
|
+
return;
|
|
4425
|
+
}
|
|
4426
|
+
const tracked = trackStream(body, run ?? captureContext(), () => ctx.abort("client"));
|
|
4427
|
+
const session = serialized.sse;
|
|
4428
|
+
const reply = adapterReply(serialized, tracked.stream, body instanceof Blob ? body : void 0);
|
|
4429
|
+
if (session !== void 0) {
|
|
4430
|
+
this.#streams.add(session);
|
|
4431
|
+
if (this.draining) session.close("shutdown");
|
|
4432
|
+
}
|
|
4433
|
+
ctx.commit();
|
|
4434
|
+
x.answer(reply);
|
|
4435
|
+
return {
|
|
4436
|
+
tracked,
|
|
4437
|
+
session
|
|
4438
|
+
};
|
|
4439
|
+
}
|
|
4440
|
+
/** A streamed body was sent or abandoned (`failure`: the stream failed). */
|
|
4441
|
+
#streamed(x, streaming, failure) {
|
|
4442
|
+
const ctx = x.ctx;
|
|
4443
|
+
if (streaming.session !== void 0) this.#streams.delete(streaming.session);
|
|
4444
|
+
if (failure !== void 0) this.#logger.error("The response stream failed", {
|
|
4445
|
+
err: failure,
|
|
4446
|
+
requestId: x.requestId,
|
|
4447
|
+
path: ctx.path
|
|
3888
4448
|
});
|
|
4449
|
+
ctx.releaseBody();
|
|
4450
|
+
}
|
|
4451
|
+
/** The request is over (response body sent or abandoned, scope closed). */
|
|
4452
|
+
#over(x) {
|
|
4453
|
+
const ctx = x.ctx;
|
|
4454
|
+
ctx.complete();
|
|
4455
|
+
try {
|
|
4456
|
+
if (!x.answered) {
|
|
4457
|
+
const failure = /* @__PURE__ */ new Error("Request not handled");
|
|
4458
|
+
const reply = serializeError(500, this.#internalBody(failure), ctx.method);
|
|
4459
|
+
const completed = this.#complete(reply, ctx, x.event, x.requestId, x.cors, x.preflight, true);
|
|
4460
|
+
x.event.status = 500;
|
|
4461
|
+
x.answer(completed);
|
|
4462
|
+
}
|
|
4463
|
+
} catch (error) {
|
|
4464
|
+
x.fail(error);
|
|
4465
|
+
} finally {
|
|
4466
|
+
this.#leave();
|
|
4467
|
+
}
|
|
3889
4468
|
}
|
|
3890
4469
|
/**
|
|
3891
4470
|
* Adds the headers of the `http:request` wrappers, then the framework headers (see
|
|
@@ -3920,7 +4499,7 @@ var Pipeline = class {
|
|
|
3920
4499
|
#checkCookies(reply, ctx, event) {
|
|
3921
4500
|
const cookies = reply.cookies;
|
|
3922
4501
|
if (cookies === void 0 || cookies.length === 0) return reply;
|
|
3923
|
-
const settings = this.#
|
|
4502
|
+
const settings = this.#cookies;
|
|
3924
4503
|
const lines = [];
|
|
3925
4504
|
try {
|
|
3926
4505
|
for (const cookie of cookies) lines.push(serializeWith(cookie, settings));
|
|
@@ -4038,41 +4617,72 @@ var Pipeline = class {
|
|
|
4038
4617
|
}
|
|
4039
4618
|
/** Runs a route: guards, body, interceptors, handler, serialisation. Never rejects (errors are mapped). */
|
|
4040
4619
|
async #invoke(ctx, plan, event) {
|
|
4620
|
+
let result;
|
|
4041
4621
|
try {
|
|
4042
4622
|
const controller = this.#resolve(plan.controller);
|
|
4043
4623
|
for (const guard of plan.guards) if (await this.#resolve(guard).canActivate(ctx) !== true) throw new ForbiddenException("Forbidden resource");
|
|
4044
4624
|
if (plan.parseBody) await ctx.parseBody();
|
|
4045
4625
|
const definition = plan.definition;
|
|
4046
4626
|
const interceptors = plan.interceptors;
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
result = definition.invoke(controller, ctx);
|
|
4050
|
-
if (isThenable(result)) result = await result;
|
|
4051
|
-
} else {
|
|
4627
|
+
if (interceptors.length === 0) result = definition.invoke(controller, ctx);
|
|
4628
|
+
else {
|
|
4052
4629
|
const chain = async (index) => {
|
|
4053
4630
|
if (index === interceptors.length) return await definition.invoke(controller, ctx);
|
|
4054
4631
|
return await this.#resolve(interceptors[index]).intercept(ctx, () => chain(index + 1));
|
|
4055
4632
|
};
|
|
4056
|
-
result =
|
|
4633
|
+
result = chain(0);
|
|
4057
4634
|
}
|
|
4058
|
-
if (definition.rawResponse === true && !(result instanceof Response)) return RAW;
|
|
4059
|
-
const options = {
|
|
4060
|
-
method: ctx.method,
|
|
4061
|
-
routeMethod: definition.method,
|
|
4062
|
-
status: definition.status,
|
|
4063
|
-
headers: definition.headers,
|
|
4064
|
-
context: ctx.hasResponseHeaders ? ctx.responseHeaders : void 0
|
|
4065
|
-
};
|
|
4066
|
-
const sse = definition.sse;
|
|
4067
|
-
let reply;
|
|
4068
|
-
if (sse !== void 0 && !(result instanceof Response) && !isHttpResponse(result)) {
|
|
4069
|
-
if (!isSseSource(result)) throw new TypeError(`@Sse handler ${displayName(plan.info.controller)}.${definition.handler} must return an AsyncIterable, an Iterable or a ReadableStream of SseEvent (received ${describeResult(result)})`);
|
|
4070
|
-
reply = serializeSseReply(this.#openStream(result, sseOptions(sse), ctx), void 0, options);
|
|
4071
|
-
} else reply = isHttpResponse(result) ? await this.#serialize(result, ctx, options, sse) : serializeResult(result, options);
|
|
4072
|
-
return this.#finish(event, reply);
|
|
4073
4635
|
} catch (error) {
|
|
4074
4636
|
return await this.#failed(error, ctx, event);
|
|
4075
4637
|
}
|
|
4638
|
+
const settled = this.#settle(ctx, plan, event, result);
|
|
4639
|
+
return settled instanceof Promise ? await settled : settled;
|
|
4640
|
+
}
|
|
4641
|
+
/**
|
|
4642
|
+
* The rest of {@link #invoke} once the handler (and its interceptors) returned `result`: awaited
|
|
4643
|
+
* when it is a thenable, then serialised. Never throws nor rejects (errors are mapped).
|
|
4644
|
+
*/
|
|
4645
|
+
#settle(ctx, plan, event, result) {
|
|
4646
|
+
if (isThenable(result)) return this.#settleLater(ctx, plan, event, result);
|
|
4647
|
+
try {
|
|
4648
|
+
const outcome = this.#outcome(ctx, plan, event, result);
|
|
4649
|
+
return outcome instanceof Promise ? outcome.catch((error) => this.#failed(error, ctx, event)) : outcome;
|
|
4650
|
+
} catch (error) {
|
|
4651
|
+
return this.#failed(error, ctx, event);
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
async #settleLater(ctx, plan, event, pending) {
|
|
4655
|
+
let result;
|
|
4656
|
+
try {
|
|
4657
|
+
result = await pending;
|
|
4658
|
+
} catch (error) {
|
|
4659
|
+
return await this.#failed(error, ctx, event);
|
|
4660
|
+
}
|
|
4661
|
+
const settled = this.#settle(ctx, plan, event, result);
|
|
4662
|
+
return settled instanceof Promise ? await settled : settled;
|
|
4663
|
+
}
|
|
4664
|
+
/**
|
|
4665
|
+
* The outcome of a handler result: `RAW` for a `@Res()` route, otherwise its reply (files resolved,
|
|
4666
|
+
* event streams opened: only an `HttpResponse` may need an asynchronous step). Throws (or rejects)
|
|
4667
|
+
* when the result cannot be served.
|
|
4668
|
+
*/
|
|
4669
|
+
#outcome(ctx, plan, event, result) {
|
|
4670
|
+
const definition = plan.definition;
|
|
4671
|
+
if (definition.rawResponse === true && !(result instanceof Response)) return RAW;
|
|
4672
|
+
const options = {
|
|
4673
|
+
method: ctx.method,
|
|
4674
|
+
routeMethod: definition.method,
|
|
4675
|
+
status: definition.status,
|
|
4676
|
+
headers: definition.headers,
|
|
4677
|
+
context: ctx.hasResponseHeaders ? ctx.responseHeaders : void 0
|
|
4678
|
+
};
|
|
4679
|
+
const sse = definition.sse;
|
|
4680
|
+
if (sse !== void 0 && !(result instanceof Response) && !isHttpResponse(result)) {
|
|
4681
|
+
if (!isSseSource(result)) throw new TypeError(`@Sse handler ${displayName(plan.info.controller)}.${definition.handler} must return an AsyncIterable, an Iterable or a ReadableStream of SseEvent (received ${describeResult(result)})`);
|
|
4682
|
+
return this.#finish(event, serializeSseReply(this.#openStream(result, sseOptions(sse), ctx), void 0, options));
|
|
4683
|
+
}
|
|
4684
|
+
if (!isHttpResponse(result)) return this.#finish(event, serializeResult(result, options));
|
|
4685
|
+
return this.#serialize(result, ctx, options, sse).then((reply) => this.#finish(event, reply));
|
|
4076
4686
|
}
|
|
4077
4687
|
/** Serialises a handler (or exception mapper) result; file bodies are resolved, event streams opened. */
|
|
4078
4688
|
async #serialize(result, ctx, options, route) {
|
|
@@ -4234,6 +4844,139 @@ var Pipeline = class {
|
|
|
4234
4844
|
}
|
|
4235
4845
|
}
|
|
4236
4846
|
};
|
|
4847
|
+
/** `#fastAsync` reads the body and runs the handler. */
|
|
4848
|
+
const NO_RESULT = Symbol("no result");
|
|
4849
|
+
/** An `http:request` wrapper returned without calling `next()` (see `Pipeline.#unhandled`). */
|
|
4850
|
+
const NOT_CALLED = Symbol("next() not called");
|
|
4851
|
+
/** A request being answered (internal): its context, its target and the promise of its reply. */
|
|
4852
|
+
var Exchange = class {
|
|
4853
|
+
request;
|
|
4854
|
+
ctx;
|
|
4855
|
+
requestId;
|
|
4856
|
+
target;
|
|
4857
|
+
preflight;
|
|
4858
|
+
replied;
|
|
4859
|
+
cors = void 0;
|
|
4860
|
+
answered = false;
|
|
4861
|
+
/** The `http:request` listeners will see the event (fast lane: keep its status). */
|
|
4862
|
+
tracked = false;
|
|
4863
|
+
/** The asynchronous part of a fast-lane request (never rejects). */
|
|
4864
|
+
done = void 0;
|
|
4865
|
+
/** Fast lane with wrappers: the outcome of the route once checked (see `Pipeline.#fastKeep`). */
|
|
4866
|
+
outcome = void 0;
|
|
4867
|
+
/** Fast lane with wrappers: `outcome` is a JSON reply or a reply without body built by the lane. */
|
|
4868
|
+
fast = false;
|
|
4869
|
+
/** Fast lane with wrappers: the async context of the handler, for a streamed body. */
|
|
4870
|
+
run = void 0;
|
|
4871
|
+
#resolve;
|
|
4872
|
+
#reject;
|
|
4873
|
+
#event;
|
|
4874
|
+
/** `startTime`: the event is created now (general pipeline); otherwise when first needed (fast lane). */
|
|
4875
|
+
constructor(request, ctx, requestId, target, startTime) {
|
|
4876
|
+
this.request = request;
|
|
4877
|
+
this.ctx = ctx;
|
|
4878
|
+
this.requestId = requestId;
|
|
4879
|
+
this.target = target;
|
|
4880
|
+
this.preflight = target.kind === "preflight";
|
|
4881
|
+
let resolve;
|
|
4882
|
+
let reject;
|
|
4883
|
+
this.replied = new Promise((onReply, onError) => {
|
|
4884
|
+
resolve = onReply;
|
|
4885
|
+
reject = onError;
|
|
4886
|
+
});
|
|
4887
|
+
this.#resolve = resolve;
|
|
4888
|
+
this.#reject = reject;
|
|
4889
|
+
if (startTime !== void 0) this.#event = requestEvent(request, ctx, startTime);
|
|
4890
|
+
}
|
|
4891
|
+
/**
|
|
4892
|
+
* The `http:request` event. The fast lane creates it when the general pipeline takes over: no
|
|
4893
|
+
* hook saw the request start, so its `startTime` is not observed.
|
|
4894
|
+
*/
|
|
4895
|
+
get event() {
|
|
4896
|
+
this.#event ??= requestEvent(this.request, this.ctx, performance.now());
|
|
4897
|
+
return this.#event;
|
|
4898
|
+
}
|
|
4899
|
+
answer(reply) {
|
|
4900
|
+
this.answered = true;
|
|
4901
|
+
this.#resolve(reply);
|
|
4902
|
+
}
|
|
4903
|
+
fail(error) {
|
|
4904
|
+
this.#reject(error);
|
|
4905
|
+
}
|
|
4906
|
+
};
|
|
4907
|
+
/** The `http:request` event of a request. */
|
|
4908
|
+
function requestEvent(request, ctx, startTime) {
|
|
4909
|
+
return {
|
|
4910
|
+
method: request.method,
|
|
4911
|
+
path: ctx.path,
|
|
4912
|
+
route: ctx.route?.path,
|
|
4913
|
+
requestId: ctx.requestId,
|
|
4914
|
+
headers: request.headers,
|
|
4915
|
+
context: ctx,
|
|
4916
|
+
startTime,
|
|
4917
|
+
responseHeaders: {}
|
|
4918
|
+
};
|
|
4919
|
+
}
|
|
4920
|
+
/** Whether a record has an own enumerable property (`event.responseHeaders` was written to). */
|
|
4921
|
+
function hasOwnKeys(record) {
|
|
4922
|
+
for (const key in record) if (Object.hasOwn(record, key)) return true;
|
|
4923
|
+
return false;
|
|
4924
|
+
}
|
|
4925
|
+
/**
|
|
4926
|
+
* The decoded parameters of a raw request path matched against a route, in one pass: what
|
|
4927
|
+
* `matchRoutePath` then `decodePathValue` (see `Pipeline.#route`) give for a path they accept.
|
|
4928
|
+
* `undefined` when the path does not match or a value is not valid percent-encoding: the general
|
|
4929
|
+
* pipeline then answers it (a reroute, a 400).
|
|
4930
|
+
*/
|
|
4931
|
+
function routeParams(route, path) {
|
|
4932
|
+
if (path.charCodeAt(0) !== 47) return void 0;
|
|
4933
|
+
const params = emptyRecord();
|
|
4934
|
+
const segments = route.segments;
|
|
4935
|
+
const last = segments.length - 1;
|
|
4936
|
+
if (last === -1) return path === "/" ? params : void 0;
|
|
4937
|
+
let position = 1;
|
|
4938
|
+
for (let index = 0; index <= last; index++) {
|
|
4939
|
+
const segment = segments[index];
|
|
4940
|
+
if (segment.kind === "wildcard") {
|
|
4941
|
+
const rest = decodePathValue(path.slice(position));
|
|
4942
|
+
if (rest === void 0) return void 0;
|
|
4943
|
+
params["*"] = rest;
|
|
4944
|
+
return params;
|
|
4945
|
+
}
|
|
4946
|
+
const slash = path.indexOf("/", position);
|
|
4947
|
+
const end = slash === -1 ? path.length : slash;
|
|
4948
|
+
if (segment.kind === "static") {
|
|
4949
|
+
const value = segment.value;
|
|
4950
|
+
if (end - position !== value.length || !path.startsWith(value, position)) return void 0;
|
|
4951
|
+
} else {
|
|
4952
|
+
if (end === position) return void 0;
|
|
4953
|
+
const decoded = decodePathValue(path.slice(position, end));
|
|
4954
|
+
if (decoded === void 0) return void 0;
|
|
4955
|
+
params[segment.name] = decoded;
|
|
4956
|
+
}
|
|
4957
|
+
if (index === last) return end === path.length ? params : void 0;
|
|
4958
|
+
if (slash === -1) return void 0;
|
|
4959
|
+
position = slash + 1;
|
|
4960
|
+
}
|
|
4961
|
+
return params;
|
|
4962
|
+
}
|
|
4963
|
+
/**
|
|
4964
|
+
* A value the fast lane serialises as JSON: a plain object or array (its own prototype and
|
|
4965
|
+
* constructor), not duck-typed as a Node.js stream. Anything `serializeResult` could treat otherwise
|
|
4966
|
+
* (`HttpResponse`, `Response`, bytes, streams, `Blob`, strings, `undefined`...) is not.
|
|
4967
|
+
*/
|
|
4968
|
+
function isPlainJson(value) {
|
|
4969
|
+
if (value === null || typeof value !== "object") return false;
|
|
4970
|
+
const prototype = Object.getPrototypeOf(value);
|
|
4971
|
+
const ctor = value.constructor;
|
|
4972
|
+
if (prototype === Object.prototype) {
|
|
4973
|
+
if (ctor !== Object) return false;
|
|
4974
|
+
} else if (prototype !== Array.prototype || ctor !== Array) return false;
|
|
4975
|
+
return typeof value.pipe !== "function";
|
|
4976
|
+
}
|
|
4977
|
+
const ENCODER = new TextEncoder();
|
|
4978
|
+
/** UTF-8 encoding of a text body, like the serialiser (a pooled `Buffer` on Node.js, `TextEncoder` on Bun). */
|
|
4979
|
+
const utf8 = isBun() ? (text) => ENCODER.encode(text) : (text) => Buffer$1.from(text, "utf8");
|
|
4237
4980
|
/**
|
|
4238
4981
|
* The reply handed to the adapter (internal fields of the serialised reply left out). A streamed
|
|
4239
4982
|
* `text/event-stream` body is an event stream whatever produced it (`@Sse`, `HttpResponse.sse()`, a
|
|
@@ -4391,6 +5134,8 @@ function validateRoute(instance, plugin, value, prefix) {
|
|
|
4391
5134
|
}
|
|
4392
5135
|
const guards = route.guards ?? [];
|
|
4393
5136
|
if (!Array.isArray(guards)) throw invalid(plugin, `the guards of the route ${where} must be an array`);
|
|
5137
|
+
const bodyLimit = route.bodyLimit === void 0 ? void 0 : parseByteSize(route.bodyLimit);
|
|
5138
|
+
if (route.bodyLimit !== void 0 && bodyLimit === void 0) throw invalid(plugin, `invalid bodyLimit ${describe(route.bodyLimit)} of the route ${where}: expected a size in bytes (1048576, '100kb', Infinity)`);
|
|
4394
5139
|
return {
|
|
4395
5140
|
plugin,
|
|
4396
5141
|
instance,
|
|
@@ -4400,7 +5145,8 @@ function validateRoute(instance, plugin, value, prefix) {
|
|
|
4400
5145
|
name: route.name,
|
|
4401
5146
|
handler: route.handler,
|
|
4402
5147
|
meta: entries,
|
|
4403
|
-
guards
|
|
5148
|
+
guards,
|
|
5149
|
+
bodyLimit
|
|
4404
5150
|
};
|
|
4405
5151
|
}
|
|
4406
5152
|
function metaEntry(plugin, where, value) {
|
|
@@ -5019,7 +5765,7 @@ var HttpPlugin = class HttpPlugin {
|
|
|
5019
5765
|
adapter.registerRoute({
|
|
5020
5766
|
method: registration.method,
|
|
5021
5767
|
path: registration.path,
|
|
5022
|
-
handler:
|
|
5768
|
+
handler: pipeline.handler(plan)
|
|
5023
5769
|
});
|
|
5024
5770
|
routes.push(Object.freeze({
|
|
5025
5771
|
method: registration.method,
|
|
@@ -5384,7 +6130,7 @@ function buildRegistrations(ctx, options, raw, pluginRoutes) {
|
|
|
5384
6130
|
invoke: (_plugin, http) => handler(http)
|
|
5385
6131
|
});
|
|
5386
6132
|
const instance = { instance: route.instance };
|
|
5387
|
-
const body = bodyPlan({}, options.bodyLimit, options.uploads);
|
|
6133
|
+
const body = bodyPlan(route.bodyLimit === void 0 ? {} : { bodyLimit: route.bodyLimit }, options.bodyLimit, options.uploads);
|
|
5388
6134
|
const chain = guards.map((guard) => guard.resolvable);
|
|
5389
6135
|
const path = route.path;
|
|
5390
6136
|
declared.push({
|
|
@@ -5671,7 +6417,8 @@ function isStandardSchema(value) {
|
|
|
5671
6417
|
* the schema strips them).
|
|
5672
6418
|
*
|
|
5673
6419
|
* Returns synchronously when the schema validates synchronously, a promise otherwise (async
|
|
5674
|
-
* refinements): generated code
|
|
6420
|
+
* refinements): generated code calls the handler directly with the value, and `await`s the value
|
|
6421
|
+
* when it is a thenable (`isThenable`).
|
|
5675
6422
|
*
|
|
5676
6423
|
* @param location what is validated, used in the error message: `body`, `query`, `query "page"`,
|
|
5677
6424
|
* `param "id"`, `headers`, `cookie "sid"`, `cookies`...
|
|
@@ -5681,8 +6428,9 @@ function isStandardSchema(value) {
|
|
|
5681
6428
|
* @throws TypeError when `schema` is not a Standard Schema v1 (a programming error → 500).
|
|
5682
6429
|
*/
|
|
5683
6430
|
function validate(schema, value, location) {
|
|
5684
|
-
|
|
5685
|
-
|
|
6431
|
+
const props = schema !== null && (typeof schema === "object" || typeof schema === "function") ? schema["~standard"] : void 0;
|
|
6432
|
+
if (props === null || typeof props !== "object" || props.version !== 1 || typeof props.validate !== "function") throw new TypeError(`The schema of ${location} is not a Standard Schema v1 (missing "~standard.validate")`);
|
|
6433
|
+
const result = props.validate(value);
|
|
5686
6434
|
if (isThenable(result)) return Promise.resolve(result).then((settled) => unwrap(settled, location));
|
|
5687
6435
|
return unwrap(result, location);
|
|
5688
6436
|
}
|
|
@@ -5707,6 +6455,6 @@ function redact(issue) {
|
|
|
5707
6455
|
path
|
|
5708
6456
|
};
|
|
5709
6457
|
}
|
|
5710
|
-
export { All, BadGatewayException, BadRequestException, Body, BodyLimit, ConflictException, Controller, Cookie, Ctx, Delete, EventChannel, ForbiddenException, GatewayTimeoutException, Get, GoneException, Head, Header, Headers, HttpCode, HttpException, HttpPlugin, HttpResponse, HttpTokens, InternalServerErrorException, MethodNotAllowedException, NotFoundException, NotImplementedException, Options, Param, Patch, PayloadTooLargeException, Post, Put, Query, Req, RequestAbortedError, Res, ServiceUnavailableException, SignedCookie, Sse, TooManyRequestsException, UnauthorizedException, UnprocessableEntityException, UnsupportedMediaTypeException, UploadedFile, UploadedFiles, UseGuard, UseInterceptor, Validate, ValidationException, coerce, contentDisposition, contentTypeFor, createRouteDecorator, encodeSseEvent, formatByteSize, isHttpException, isHttpResponse, isJsonType, isStandardSchema, isValidStatus, matchesMediaType, nodeAdapter, parseBody, parseByteSize, parseCookieHeader, parseMediaType, parseQuery, parseRange, parseSetCookie, parseSseStream, serializeClearCookie, serializeCookie, statusText, toIssueDetail, uploads, validate };
|
|
6458
|
+
export { All, BadGatewayException, BadRequestException, Body, BodyLimit, ConflictException, Controller, Cookie, Ctx, Delete, EventChannel, ForbiddenException, GatewayTimeoutException, Get, GoneException, Head, Header, Headers, HttpCode, HttpException, HttpPlugin, HttpResponse, HttpTokens, InternalServerErrorException, MethodNotAllowedException, NotFoundException, NotImplementedException, Options, Param, Patch, PayloadTooLargeException, Post, Put, Query, Req, RequestAbortedError, Res, ServiceUnavailableException, SignedCookie, Sse, TooManyRequestsException, UnauthorizedException, UnprocessableEntityException, UnsupportedMediaTypeException, UploadedFile, UploadedFiles, UseGuard, UseInterceptor, Validate, ValidationException, coerce, contentDisposition, contentTypeFor, createRouteDecorator, encodeSseEvent, formatByteSize, isHttpException, isHttpResponse, isJsonType, isStandardSchema, isThenable, isValidStatus, matchesMediaType, nodeAdapter, parseBody, parseByteSize, parseCookieHeader, parseMediaType, parseQuery, parseRange, parseSetCookie, parseSseStream, serializeClearCookie, serializeCookie, statusText, toIssueDetail, uploads, validate };
|
|
5711
6459
|
|
|
5712
6460
|
//# sourceMappingURL=index.mjs.map
|