@znight/http 0.1.1 → 0.1.3

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.
Files changed (47) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +83 -32
  3. package/dist/{adapter-CGSceVEN.d.mts → adapter-J0UXFnNM.d.mts} +7 -1
  4. package/dist/compiler.d.mts +1 -1
  5. package/dist/compiler.mjs +82 -13
  6. package/dist/compiler.mjs.map +1 -1
  7. package/dist/express.d.mts +1 -1
  8. package/dist/express.mjs +3 -2
  9. package/dist/express.mjs.map +1 -1
  10. package/dist/fastify.d.mts +1 -1
  11. package/dist/fastify.mjs +254 -42
  12. package/dist/fastify.mjs.map +1 -1
  13. package/dist/hono.d.mts +1 -1
  14. package/dist/hono.mjs +272 -67
  15. package/dist/hono.mjs.map +1 -1
  16. package/dist/index.d.mts +10 -2
  17. package/dist/index.mjs +930 -278
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/{node-BUnkdSsa.mjs → node-DnYv_AMI.mjs} +4 -3
  20. package/dist/{node-BUnkdSsa.mjs.map → node-DnYv_AMI.mjs.map} +1 -1
  21. package/dist/{node-io-BY1ZDLRQ.mjs → node-io-BkdRSQeg.mjs} +151 -86
  22. package/dist/{node-io-BY1ZDLRQ.mjs.map → node-io-BkdRSQeg.mjs.map} +1 -1
  23. package/dist/node.d.mts +1 -1
  24. package/dist/node.mjs +1 -1
  25. package/dist/utils-DIWdzVht.mjs +46 -0
  26. package/dist/utils-DIWdzVht.mjs.map +1 -0
  27. package/package.json +4 -4
  28. package/src/adapter.ts +6 -0
  29. package/src/adapters/express.ts +4 -1
  30. package/src/adapters/fastify.ts +329 -44
  31. package/src/adapters/hono.ts +341 -86
  32. package/src/adapters/node-io.ts +185 -82
  33. package/src/adapters/node.ts +4 -1
  34. package/src/body.ts +31 -2
  35. package/src/coerce.ts +31 -0
  36. package/src/compiler/generate.ts +127 -22
  37. package/src/context.ts +151 -78
  38. package/src/index.ts +1 -1
  39. package/src/pipeline.ts +809 -208
  40. package/src/plugin.ts +10 -4
  41. package/src/query.ts +7 -1
  42. package/src/response.ts +6 -8
  43. package/src/serialize.ts +83 -16
  44. package/src/utils.ts +25 -2
  45. package/src/validate.ts +28 -12
  46. package/dist/utils-DCbu3a4w.mjs +0 -27
  47. 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-BUnkdSsa.mjs";
2
- import { $ as UnprocessableEntityException, A as serializeCookie, B as ConflictException, C as decodeCookieValue, D as parseCookieHeader, E as isCookieName, F as isStreamSource, G as InternalServerErrorException, H as GatewayTimeoutException, I as toByteStream, J as NotImplementedException, K as MethodNotAllowedException, L as trackStream, M as captureContext, N as discardStream, O as parseSetCookie, P as isByteStream, Q as UnauthorizedException, R as BadGatewayException, S as checkCookie, T as givenCookieKey, U as GoneException, V as ForbiddenException, W as HttpException, X as ServiceUnavailableException, Y as PayloadTooLargeException, Z as TooManyRequestsException, at as isBodylessStatus, et as UnsupportedMediaTypeException, h as isEventStreamType, it as toIssueDetail, j as serializeResponseCookie, k as serializeClearCookie, nt as isHttpException, ot as isValidStatus, q as NotFoundException, rt as issue, st as statusText, tt as ValidationException, w as encodeCookieValue, x as DEFAULT_COOKIE_OPTIONS, z as BadRequestException } from "./node-io-BY1ZDLRQ.mjs";
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 { i as isThenable, n as errorMessage, t as emptyRecord } from "./utils-DCbu3a4w.mjs";
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(parseBytes(bytes, contentType, contentEncoding));
566
+ return Promise.resolve(parseBodyBytes(bytes, contentType, contentEncoding));
541
567
  } catch (error) {
542
568
  return Promise.reject(error);
543
569
  }
544
570
  }
545
- function parseBytes(bytes, contentType, contentEncoding) {
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
- #cookies;
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
- #abortReason;
821
- #abortController;
822
- constructor(source, init) {
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 = init.body;
825
- this.#cookieSettings = init.cookies ?? DEFAULT_COOKIE_SETTINGS;
826
- const { path, query } = splitUrl(source.url);
890
+ this.#plan = body;
891
+ this.#cookieSettings = cookies;
827
892
  this.#rawQuery = query;
828
- this.requestId = init.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 = init.params;
897
+ this.params = params;
833
898
  this.headers = source.headers;
834
- this.route = init.route;
899
+ this.route = route;
835
900
  this.request = source.raw;
836
901
  this.response = source.rawResponse;
837
- this.adapter = init.adapter;
838
- const connection = source.encrypted === true ? "https" : "http";
839
- this.ip = init.client === void 0 ? source.ip : init.client.ip;
840
- this.protocol = init.client === void 0 ? connection : init.client.protocol;
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
- this.#query ??= parseQuery(this.#rawQuery);
844
- return this.#query;
922
+ const state = this.#extra;
923
+ state.query ??= parseQuery(this.#rawQuery);
924
+ return state.query;
845
925
  }
846
926
  get state() {
847
- this.#state ??= /* @__PURE__ */ new Map();
848
- return this.#state;
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
- this.#cookies ??= parseCookieHeader(this.headers["cookie"]);
883
- return this.#cookies;
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
- this.#signed ??= /* @__PURE__ */ new Map();
889
- if (this.#signed.has(name)) return this.#signed.get(name);
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
- this.#signed.set(name, value);
974
+ state.signed.set(name, value);
892
975
  return value;
893
976
  }
894
977
  setCookie(name, value, options = {}) {
@@ -908,9 +991,13 @@ 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
- return this.#jar === void 0 ? [] : [...this.#jar].map(([key, entry]) => ({
1000
+ return this.#jar === void 0 ? NO_LINES : [...this.#jar].map(([key, entry]) => ({
914
1001
  key,
915
1002
  line: entry.line
916
1003
  }));
@@ -932,27 +1019,29 @@ var RequestContext = class {
932
1019
  });
933
1020
  }
934
1021
  get signal() {
935
- if (this.#abortController === void 0) {
1022
+ const state = this.#extra;
1023
+ if (state.abortController === void 0) {
936
1024
  const controller = new AbortController();
937
- this.#abortController = controller;
938
- if (this.#abortReason !== void 0) controller.abort(this.#abortReason);
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 this.#abortController.signal;
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
- this.#abortReason = reason;
955
- this.#abortController?.abort(reason);
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
- if (this.#raw === void 0) {
967
- if (this.#streamed) return Promise.reject(alreadyStreamed());
968
- this.#raw = this.#source.readBody(this.#plan.limit);
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 this.#raw;
1060
+ return state.raw;
971
1061
  }
972
1062
  bodyStream() {
973
- if (this.#raw !== void 0 || this.#streamed) throw alreadyStreamed();
974
- this.#streamed = true;
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 stop = this.#stopBody;
984
- this.#stopBody = void 0;
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
- this.#stopBody = void 0;
1086
+ state.stopBody = void 0;
993
1087
  };
994
- this.#stopBody = () => {
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
- this.#parsed ??= this.#parse();
1027
- return this.#parsed;
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 (this.#raw === void 0) {
1038
- if (this.#streamed) throw alreadyStreamed();
1039
- this.#streamed = true;
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 this.#raw, contentType, policy);
1042
- this.#files = result.files;
1043
- this.#body = result.fields;
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 = await parseBody(await this.rawBody(), contentType, headers["content-encoding"]);
1051
- this.#body = body;
1146
+ const body = parseBodyBytes(await this.rawBody(), contentType, headers["content-encoding"]);
1147
+ state.body = body;
1052
1148
  return body;
1053
1149
  }
1054
1150
  };
@@ -1071,6 +1167,7 @@ function withoutLifetime(options) {
1071
1167
  }
1072
1168
  const EMPTY_HEADERS = Object.freeze({});
1073
1169
  const NO_COOKIES$1 = Object.freeze([]);
1170
+ const NO_LINES = Object.freeze([]);
1074
1171
  const NO_FILES$1 = Object.freeze(emptyRecord());
1075
1172
  // src/decorators.ts
1076
1173
  function marker() {}
@@ -1787,7 +1884,7 @@ function encodeExtValue(value) {
1787
1884
  */
1788
1885
  /** Length of a signature: base64url of 32 bytes, without padding. */
1789
1886
  const SIGNATURE_LENGTH = 43;
1790
- const ENCODER$2 = new TextEncoder();
1887
+ const ENCODER$3 = new TextEncoder();
1791
1888
  /**
1792
1889
  * Signs and verifies cookie values. The wire form of a signed cookie is
1793
1890
  * `<encoded value>.<signature>` with `signature = base64url(HMAC-SHA256(secret, "<name>=<encoded value>"))`:
@@ -1834,7 +1931,7 @@ function isSignature(text) {
1834
1931
  return true;
1835
1932
  }
1836
1933
  function toBytes(secret) {
1837
- return typeof secret === "string" ? ENCODER$2.encode(secret) : secret;
1934
+ return typeof secret === "string" ? ENCODER$3.encode(secret) : secret;
1838
1935
  }
1839
1936
  /**
1840
1937
  * Normalises the option `cookies.secrets` to a list, or explains what is wrong with it (a secret
@@ -2850,8 +2947,8 @@ function describe$1(value) {
2850
2947
  function errorText(error) {
2851
2948
  return error instanceof Error ? error.message : String(error);
2852
2949
  }
2853
- const ENCODER$1 = new TextEncoder();
2854
- const HEARTBEAT = ENCODER$1.encode(":\n\n");
2950
+ const ENCODER$2 = new TextEncoder();
2951
+ const HEARTBEAT = ENCODER$2.encode(":\n\n");
2855
2952
  /**
2856
2953
  * One event stream: the byte stream sent to the client and the controls of the pipeline
2857
2954
  * ({@link close} on shutdown or `maxDuration`). The source is read only when the client pulls
@@ -2901,7 +2998,7 @@ var SseSession = class {
2901
2998
  start: (controller) => {
2902
2999
  this.#controller = controller;
2903
3000
  const retry = settings.retry;
2904
- controller.enqueue(retry === void 0 ? HEARTBEAT : ENCODER$1.encode(`retry: ${retry}\n\n`));
3001
+ controller.enqueue(retry === void 0 ? HEARTBEAT : ENCODER$2.encode(`retry: ${retry}\n\n`));
2905
3002
  if (settings.maxDuration !== void 0) this.#maxTimer = unref(setTimeout(() => this.close("expired"), timerDelay(settings.maxDuration)));
2906
3003
  },
2907
3004
  pull: (controller) => this.#pull(controller),
@@ -2964,7 +3061,7 @@ var SseSession = class {
2964
3061
  await this.#finish(true);
2965
3062
  return;
2966
3063
  }
2967
- controller.enqueue(ENCODER$1.encode(encoded));
3064
+ controller.enqueue(ENCODER$2.encode(encoded));
2968
3065
  this.#atBoundary = true;
2969
3066
  this.#lineHasContent = false;
2970
3067
  this.#afterCr = false;
@@ -3092,7 +3189,7 @@ var SseSession = class {
3092
3189
  #finalRetry() {
3093
3190
  const base = this.#settings.retry ?? 3e3;
3094
3191
  const random = this.#settings.random ?? Math.random;
3095
- return ENCODER$1.encode(`retry: ${Math.round(base + random() * base)}\n\n`);
3192
+ return ENCODER$2.encode(`retry: ${Math.round(base + random() * base)}\n\n`);
3096
3193
  }
3097
3194
  /**
3098
3195
  * Stops the source (`return()` / `cancel()`) and waits for it at most `stopTimeoutMs`: an async
@@ -3343,10 +3440,19 @@ function withCookie(cookies, cookie) {
3343
3440
  }
3344
3441
  /** Whether `value` is an {@link HttpResponse} (also across duplicated package copies). */
3345
3442
  function isHttpResponse(value) {
3346
- return value instanceof HttpResponse || value !== null && typeof value === "object" && value.constructor?.name === "HttpResponse" && "body" in value && "headers" in value;
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;
3347
3448
  }
3348
3449
  // src/serialize.ts
3349
- const ENCODER = new TextEncoder();
3450
+ const ENCODER$1 = new TextEncoder();
3451
+ /**
3452
+ * UTF-8 encoding of a text body: a pooled `Buffer` (a `Uint8Array`) is several times cheaper than
3453
+ * `TextEncoder` on Node.js, `TextEncoder` is the fastest on Bun.
3454
+ */
3455
+ const utf8$1 = isBun() ? (text) => ENCODER$1.encode(text) : (text) => Buffer$1.from(text, "utf8");
3350
3456
  /** Content types set by the serialiser. */
3351
3457
  const ContentType = Object.freeze({
3352
3458
  json: "application/json; charset=utf-8",
@@ -3385,12 +3491,28 @@ function serializeResult(result, options) {
3385
3491
  cookies = cookiesOf(result);
3386
3492
  }
3387
3493
  const finalStatus = options.status ?? status ?? defaultStatus(options.routeMethod, payload);
3388
- const headers = emptyRecord();
3389
- const body = encode(payload, headers);
3390
- if (options.headers !== void 0) assignHeaders(headers, options.headers);
3391
- if (options.context !== void 0) assignHeaders(headers, options.context);
3392
- if (extra !== void 0) assignHeaders(headers, extra);
3393
- return withCookies(finish(finalStatus, headers, body, options.method), cookies);
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;
3502
+ let own = true;
3503
+ if (options.headers !== void 0) {
3504
+ assignHeaders(headers, options.headers);
3505
+ own = false;
3506
+ }
3507
+ if (options.context !== void 0) {
3508
+ assignHeaders(headers, options.context);
3509
+ own = false;
3510
+ }
3511
+ if (extra !== void 0) {
3512
+ assignHeaders(headers, extra);
3513
+ own = false;
3514
+ }
3515
+ return withCookies(finish(finalStatus, headers, body, options.method, text, own), cookies);
3394
3516
  }
3395
3517
  /**
3396
3518
  * Serialises an event stream (`@Sse` result, `HttpResponse.sse()`): status 200 unless `@HttpCode`
@@ -3401,7 +3523,7 @@ function serializeResult(result, options) {
3401
3523
  */
3402
3524
  function serializeSseReply(session, response, options) {
3403
3525
  const status = options.status ?? response?.status ?? 200;
3404
- const headers = emptyRecord();
3526
+ const headers = fastRecord();
3405
3527
  headers["cache-control"] = "no-cache, no-transform";
3406
3528
  headers["x-accel-buffering"] = "no";
3407
3529
  if (options.headers !== void 0) assignHeaders(headers, options.headers);
@@ -3434,7 +3556,7 @@ function withCookies(reply, cookies) {
3434
3556
  */
3435
3557
  function serializeFileReply(file, response, options) {
3436
3558
  const status = file.status === 200 ? options.status ?? response.status ?? 200 : file.status;
3437
- const headers = emptyRecord();
3559
+ const headers = fastRecord();
3438
3560
  assignHeaders(headers, file.headers);
3439
3561
  if (options.headers !== void 0) assignHeaders(headers, options.headers);
3440
3562
  if (options.context !== void 0) assignHeaders(headers, options.context);
@@ -3448,8 +3570,10 @@ function defaultStatus(routeMethod, payload) {
3448
3570
  }
3449
3571
  /** Serialises an error body as JSON. */
3450
3572
  function serializeError(status, body, method) {
3451
- const headers = emptyRecord();
3452
- return finish(status, headers, encode(body, headers), method);
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);
3453
3577
  }
3454
3578
  /** Copies headers with lower-case names (later values win). */
3455
3579
  function assignHeaders(target, source) {
@@ -3466,7 +3590,11 @@ function addMissingHeaders(target, source) {
3466
3590
  if (value !== void 0 && target[key] === void 0) target[key] = typeof value === "string" ? value : [...value];
3467
3591
  }
3468
3592
  }
3469
- function finish(status, headers, body, method) {
3593
+ /**
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.
3596
+ */
3597
+ function finish(status, headers, body, method, text, own = false) {
3470
3598
  if (isBodylessStatus(status)) {
3471
3599
  discard(body);
3472
3600
  delete headers["content-type"];
@@ -3485,18 +3613,53 @@ function finish(status, headers, body, method) {
3485
3613
  body: null
3486
3614
  };
3487
3615
  }
3488
- return {
3616
+ if (text === void 0) return {
3489
3617
  status,
3490
3618
  headers,
3491
3619
  body
3492
3620
  };
3621
+ return own ? {
3622
+ status,
3623
+ headers,
3624
+ body,
3625
+ text,
3626
+ ownHeaders: true
3627
+ } : {
3628
+ status,
3629
+ headers,
3630
+ body,
3631
+ text
3632
+ };
3493
3633
  }
3494
3634
  function discard(body) {
3495
3635
  if (body instanceof ReadableStream) discardStream(body);
3496
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
+ }
3641
+ /**
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.
3659
+ */
3497
3660
  function encode(payload, headers) {
3498
3661
  if (payload === void 0) return null;
3499
- if (typeof payload === "string") return bytes(ENCODER.encode(payload), headers, ContentType.text);
3662
+ if (typeof payload === "string") return text$1(payload, headers, ContentType.text);
3500
3663
  if (payload instanceof Uint8Array) return bytes(payload, headers, ContentType.bytes);
3501
3664
  if (payload instanceof ArrayBuffer) return bytes(new Uint8Array(payload), headers, ContentType.bytes);
3502
3665
  if (ArrayBuffer.isView(payload)) return bytes(new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength), headers, ContentType.bytes);
@@ -3517,18 +3680,22 @@ function encode(payload, headers) {
3517
3680
  if (isSseBody(payload)) throw new TypeError("An event stream (HttpResponse.sse()) cannot be serialised here");
3518
3681
  const json = JSON.stringify(payload);
3519
3682
  if (json === void 0) throw new TypeError(`Cannot serialise a ${typeof payload} as a response body`);
3520
- return bytes(ENCODER.encode(json), headers, ContentType.json);
3683
+ return text$1(json, headers, ContentType.json);
3521
3684
  }
3522
3685
  function bytes(data, headers, type) {
3523
3686
  headers["content-type"] = type;
3524
3687
  headers["content-length"] = String(data.byteLength);
3525
3688
  return data;
3526
3689
  }
3690
+ function text$1(data, headers, type) {
3691
+ headers["content-type"] = type;
3692
+ return data;
3693
+ }
3527
3694
  function isNodeReadable(value) {
3528
3695
  return value instanceof Readable || value !== null && typeof value === "object" && typeof value.pipe === "function" && typeof value.on === "function" && typeof value.read === "function";
3529
3696
  }
3530
3697
  function fromWebResponse(response, method) {
3531
- const headers = emptyRecord();
3698
+ const headers = fastRecord();
3532
3699
  for (const [name, value] of response.headers) if (name !== "set-cookie") headers[name] = value;
3533
3700
  const cookies = response.headers.getSetCookie();
3534
3701
  if (cookies.length > 0) headers["set-cookie"] = cookies;
@@ -3555,6 +3722,63 @@ const DEFAULT_SSE_SETTINGS = Object.freeze({
3555
3722
  const RAW = Symbol("raw response");
3556
3723
  const REQUEST_ID = /^[\w\-.:@/+=]{1,128}$/;
3557
3724
  const MAX_PATH_IN_MESSAGE = 200;
3725
+ /** The default request id generator: a random UUID (version 4, like `crypto.randomUUID()`). */
3726
+ function randomRequestId() {
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];
3743
+ }
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;
3558
3782
  /**
3559
3783
  * The request pipeline (architecture §9.1):
3560
3784
  * `runInScope({ HttpContext, RequestId }) → hooks.run('http:request') → guards → body → interceptors
@@ -3566,6 +3790,26 @@ var Pipeline = class {
3566
3790
  #logger;
3567
3791
  #options;
3568
3792
  #table;
3793
+ /** The `securityHeaders` option as entries (lower-case names, values validated at boot). */
3794
+ #securityHeaders;
3795
+ /** Every request id can be sent: an incoming one matched `REQUEST_ID`, the generated ones are UUIDs. */
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;
3569
3813
  #inflight = 0;
3570
3814
  #idle = [];
3571
3815
  /** Open event streams. */
@@ -3578,6 +3822,23 @@ var Pipeline = class {
3578
3822
  this.#logger = logger;
3579
3823
  this.#options = options;
3580
3824
  this.#table = table;
3825
+ this.#validIds = options.generateRequestId === randomRequestId;
3826
+ this.#cookies = options.cookies ?? DEFAULT_COOKIE_SETTINGS;
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));
3581
3842
  }
3582
3843
  /** Number of requests being processed. */
3583
3844
  get inflight() {
@@ -3600,31 +3861,229 @@ var Pipeline = class {
3600
3861
  return new Promise((resolve) => this.#idle.push(resolve));
3601
3862
  }
3602
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 no
3893
+ * `http:request` wrapper nor listener is registered, the server is not draining and its method
3894
+ * is the route's (not a `HEAD` served by a `GET` route); a plain object or array returned by the
3895
+ * handler is serialised as JSON, and `undefined` answered without body, with the headers in their
3896
+ * final order (a template precomputed at boot).
3897
+ * A thenable result is awaited in the lane (see {@link #fastAsync}). Anything else — a malformed
3898
+ * path (before the request starts), a thrown error or a rejection, an `HttpResponse`, a `Response`
3899
+ * or any other result, headers or cookies set through the context — continues in the general
3900
+ * pipeline, in the same request scope, with the same outcome.
3901
+ */
3902
+ #fast(route, request) {
3903
+ const plan = route.plan;
3904
+ const hooks = this.#hooks;
3905
+ if (this.draining || request.method !== route.method || hooks.hasWrappers("http:request") || hooks.hasListeners("http:request")) return this.handle(plan, request);
3906
+ const url = request.url;
3907
+ const mark = url.indexOf("?");
3908
+ const path = mark === -1 ? url : url.slice(0, mark);
3909
+ const params = routeParams(plan.path, path);
3910
+ if (params === void 0) return this.handle(plan, request);
3911
+ this.#enter();
3912
+ let ctx;
3913
+ try {
3914
+ const query = mark === -1 ? "" : url.slice(mark + 1);
3915
+ ctx = this.#context(request, this.#requestId(request), path, query, plan.info, params, plan.body);
3916
+ } catch (error) {
3917
+ this.#leave();
3918
+ return Promise.reject(error);
3919
+ }
3920
+ const scope = this.#container.openScope([[HttpTokens.Context, ctx], [HttpTokens.RequestId, ctx.requestId]]);
3921
+ const done = scope.run(this.#fastRun, route, request, ctx);
3922
+ if (done instanceof Exchange) {
3923
+ done.done.then(() => this.#end(done, scope, void 0), (error) => this.#end(done, scope, error));
3924
+ if (!hooks.hasListeners("http:request")) return done.replied;
3925
+ done.tracked = true;
3926
+ return this.#emitted(done);
3927
+ }
3928
+ const closing = scope.close();
3929
+ if (closing === void 0) {
3930
+ ctx.complete();
3931
+ this.#leave();
3932
+ } else {
3933
+ const x = new Exchange(request, ctx, ctx.requestId, route.target);
3934
+ x.answer(done);
3935
+ closing.then(() => this.#over(x), (error) => this.#disposalFailed(x, error));
3936
+ }
3937
+ if (!hooks.hasListeners("http:request")) return Promise.resolve(done);
3938
+ const event = requestEvent(request, ctx, performance.now());
3939
+ event.status = done.status;
3940
+ return Promise.resolve(done).then((reply) => {
3941
+ hooks.emit("http:request", event);
3942
+ return reply;
3943
+ });
3944
+ }
3945
+ /**
3946
+ * The fast lane in the request scope: the reply when the handler answered synchronously with a
3947
+ * plain JSON value or `undefined`, otherwise the exchange finishing the request (see {@link #fast}).
3948
+ */
3949
+ #fastRun = (route, request, ctx) => {
3950
+ const plan = route.plan;
3951
+ if (plan.parseBody) return this.#fastLater(route, request, ctx, NO_RESULT);
3952
+ let result;
3953
+ let reply;
3954
+ try {
3955
+ result = plan.definition.invoke(this.#resolve(plan.controller), ctx);
3956
+ if (isThenable(result)) return this.#fastLater(route, request, ctx, result);
3957
+ reply = this.#fastReply(route, ctx, result);
3958
+ } catch (error) {
3959
+ const x = new Exchange(request, ctx, ctx.requestId, route.target);
3960
+ x.done = this.#respond(x, () => this.#failed(error, ctx, x.event));
3961
+ return x;
3962
+ }
3963
+ if (reply !== void 0) {
3964
+ ctx.commit();
3965
+ ctx.releaseBody();
3966
+ return reply;
3967
+ }
3968
+ const x = new Exchange(request, ctx, ctx.requestId, route.target);
3969
+ x.done = this.#respond(x, () => this.#settle(ctx, plan, x.event, result));
3970
+ return x;
3971
+ };
3972
+ /** The fast lane of a route with a body to read or a handler that returned a thenable. */
3973
+ #fastLater(route, request, ctx, result) {
3974
+ const x = new Exchange(request, ctx, ctx.requestId, route.target);
3975
+ x.done = this.#fastAsync(x, route, result);
3976
+ return x;
3977
+ }
3978
+ /**
3979
+ * The asynchronous part of the fast lane, in the request scope: the body read (`result` is
3980
+ * {@link NO_RESULT}) and the handler run, or the thenable it returned awaited. Never rejects.
3981
+ */
3982
+ async #fastAsync(x, route, pending) {
3983
+ const ctx = x.ctx;
3984
+ const plan = route.plan;
3985
+ let result = pending;
3986
+ let reply;
3987
+ try {
3988
+ if (result === NO_RESULT) {
3989
+ const controller = this.#resolve(plan.controller);
3990
+ await ctx.parseBody();
3991
+ result = plan.definition.invoke(controller, ctx);
3992
+ }
3993
+ if (isThenable(result)) result = await result;
3994
+ reply = this.#fastReply(route, ctx, result);
3995
+ } catch (error) {
3996
+ await this.#respond(x, () => this.#failed(error, ctx, x.event));
3997
+ return;
3998
+ }
3999
+ if (reply === void 0) {
4000
+ const value = result;
4001
+ await this.#respond(x, () => this.#settle(ctx, plan, x.event, value));
4002
+ return;
4003
+ }
4004
+ ctx.commit();
4005
+ ctx.releaseBody();
4006
+ if (x.tracked) x.event.status = reply.status;
4007
+ x.answer(reply);
4008
+ }
4009
+ /**
4010
+ * The reply of a plain JSON value (see {@link isPlainJson}) or of `undefined`: what
4011
+ * `serializeResult` and {@link #complete} produce for it, the headers in their final order.
4012
+ * `undefined` when the result or the context needs the general pipeline. Throws like
4013
+ * `JSON.stringify`.
4014
+ */
4015
+ #fastReply(route, ctx, result) {
4016
+ const status = result === void 0 ? route.emptyStatus : route.jsonStatus;
4017
+ if (status === void 0 || ctx.hasResponseHeaders || ctx.hasCookies || this.draining) return void 0;
4018
+ const requestId = ctx.requestId;
4019
+ if (!this.#validIds && !isHeaderValue(requestId)) return void 0;
4020
+ if (result === void 0) {
4021
+ const headers = { ...this.#emptyHeaders };
4022
+ const idHeader = this.#emptyIdHeader;
4023
+ if (idHeader !== void 0) headers[idHeader] = requestId;
4024
+ return {
4025
+ status,
4026
+ headers,
4027
+ body: null
4028
+ };
4029
+ }
4030
+ if (!isPlainJson(result)) return void 0;
4031
+ const json = JSON.stringify(result);
4032
+ if (json === void 0) throw new TypeError(`Cannot serialise a ${typeof result} as a response body`);
4033
+ const headers = { ...this.#jsonHeaders };
4034
+ const body = utf8(json);
4035
+ headers["content-length"] = String(body.byteLength);
4036
+ const idHeader = this.#jsonIdHeader;
4037
+ if (idHeader !== void 0) headers[idHeader] = requestId;
4038
+ return {
4039
+ status,
4040
+ headers,
4041
+ body,
4042
+ text: json
4043
+ };
4044
+ }
4045
+ /** Ends a fast-lane request once its asynchronous part settled: its scope is closed, then it is over. */
4046
+ #end(x, scope, error) {
4047
+ const closing = scope.close();
4048
+ if (error !== void 0) {
4049
+ const report = () => this.#disposalFailed(x, error);
4050
+ if (closing === void 0) report();
4051
+ else closing.then(report, report);
4052
+ return;
4053
+ }
4054
+ if (closing === void 0) this.#over(x);
4055
+ else closing.then(() => this.#over(x), (failure) => this.#disposalFailed(x, failure));
4056
+ }
4057
+ #disposalFailed(x, error) {
4058
+ this.#over(x);
4059
+ this.#logger.error("Failed to dispose request-scoped providers", {
4060
+ err: error,
4061
+ requestId: x.requestId
4062
+ });
4063
+ }
4064
+ /**
3603
4065
  * Handles a request routed to `plan` by the adapter. The request stays in flight (and its scope
3604
4066
  * open) until a streamed or file response body has been fully read or cancelled by the adapter.
3605
4067
  */
3606
- async handle(plan, request) {
4068
+ handle(plan, request) {
3607
4069
  const startTime = performance.now();
3608
- this.#enter();
3609
- let served;
3610
4070
  try {
3611
- served = this.#isPreflight(request) ? await this.#preflight(request, startTime) : await this.#route(plan, request, startTime);
3612
- return served.reply;
3613
- } finally {
3614
- this.#leaveWhen(served);
4071
+ return this.#isPreflight(request) ? this.#preflight(request, startTime) : this.#route(plan, request, startTime);
4072
+ } catch (error) {
4073
+ return Promise.reject(error);
3615
4074
  }
3616
4075
  }
3617
4076
  #route(plan, request, startTime, rerouted = false) {
3618
- const { path, query } = splitUrl(request.url);
3619
- const raw = matchRoutePath(plan.path, path);
4077
+ const url = splitUrl(request.url);
4078
+ const raw = matchRoutePath(plan.path, url.path);
3620
4079
  if (raw === void 0) return this.#fallback(request, void 0, startTime, !rerouted);
3621
4080
  if (plan.redirect === true) {
3622
- const canonical = sameOriginPath(path.slice(0, -1));
3623
- const location = query === "" ? canonical : `${canonical}?${query}`;
3624
- return this.#serve(request, void 0, emptyRecord(), {
4081
+ const canonical = sameOriginPath(url.path.slice(0, -1));
4082
+ const target = {
3625
4083
  kind: "redirect",
3626
- location
3627
- }, startTime, plan.body);
4084
+ location: url.query === "" ? canonical : `${canonical}?${url.query}`
4085
+ };
4086
+ return this.#serve(request, url, void 0, emptyRecord(), target, startTime, plan.body);
3628
4087
  }
3629
4088
  const params = emptyRecord();
3630
4089
  let malformed;
@@ -3640,18 +4099,15 @@ var Pipeline = class {
3640
4099
  kind: "error",
3641
4100
  error: new BadRequestException(`Malformed URL parameter "${malformed}"`)
3642
4101
  };
3643
- return this.#serve(request, plan.info, params, target, startTime, plan.body);
4102
+ return this.#serve(request, url, plan.info, params, target, startTime, plan.body);
3644
4103
  }
3645
4104
  /** Handles an unmatched request (404, 405, automatic OPTIONS) or a framework error. */
3646
- async fallback(request, error) {
4105
+ fallback(request, error) {
3647
4106
  const startTime = performance.now();
3648
- this.#enter();
3649
- let served;
3650
4107
  try {
3651
- served = await this.#fallback(request, error, startTime);
3652
- return served.reply;
3653
- } finally {
3654
- this.#leaveWhen(served);
4108
+ return this.#fallback(request, error, startTime);
4109
+ } catch (failure) {
4110
+ return Promise.reject(failure);
3655
4111
  }
3656
4112
  }
3657
4113
  #fallback(request, error, startTime, reroute = true) {
@@ -3661,10 +4117,11 @@ var Pipeline = class {
3661
4117
  kind: "error",
3662
4118
  error: frameworkError(error)
3663
4119
  };
3664
- return this.#serve(request, void 0, emptyRecord(), target, startTime, body);
4120
+ return this.#serve(request, splitUrl(request.url), void 0, emptyRecord(), target, startTime, body);
3665
4121
  }
3666
4122
  if (this.#isPreflight(request)) return this.#preflight(request, startTime);
3667
- const { path } = splitUrl(request.url);
4123
+ const url = splitUrl(request.url);
4124
+ const path = url.path;
3668
4125
  if (reroute) {
3669
4126
  const plan = this.#table.find(request.method, path);
3670
4127
  if (plan !== void 0) return this.#route(plan, request, startTime, true);
@@ -3683,151 +4140,169 @@ var Pipeline = class {
3683
4140
  kind: "error",
3684
4141
  error: new MethodNotAllowedException(`Method ${request.method} is not allowed on ${truncate(path)}`, void 0, { headers: { allow: allowHeader(allowed) } })
3685
4142
  };
3686
- return this.#serve(request, void 0, emptyRecord(), target, startTime, body);
4143
+ return this.#serve(request, url, void 0, emptyRecord(), target, startTime, body);
3687
4144
  }
3688
4145
  /** A CORS preflight (with the `cors` option): answered without running any route. */
3689
4146
  #isPreflight(request) {
3690
4147
  return this.#options.cors !== void 0 && isPreflight(request.method, request.headers);
3691
4148
  }
3692
4149
  #preflight(request, startTime) {
3693
- const allowed = this.#table.allowedMethods(splitUrl(request.url).path);
4150
+ const url = splitUrl(request.url);
4151
+ const allowed = this.#table.allowedMethods(url.path);
3694
4152
  const target = {
3695
4153
  kind: "preflight",
3696
4154
  allow: allowed.length === 0 ? void 0 : allowHeader(allowed)
3697
4155
  };
3698
- return this.#serve(request, void 0, emptyRecord(), target, startTime, this.#options.body);
4156
+ return this.#serve(request, url, void 0, emptyRecord(), target, startTime, this.#options.body);
3699
4157
  }
3700
- async #serve(request, route, params, target, startTime, body) {
3701
- const requestId = this.#requestId(request);
3702
- const proxy = this.#options.trustProxy;
3703
- const ctx = new RequestContext(request, {
3704
- requestId,
3705
- adapter: this.#options.adapter,
3706
- route,
3707
- params,
3708
- body,
3709
- cookies: this.#options.cookies ?? DEFAULT_COOKIE_SETTINGS,
3710
- ...proxy === void 0 ? {} : { client: proxy.resolve(request.remoteAddress ?? request.ip, request.headers, request.encrypted === true) }
4158
+ #serve(request, url, route, params, target, startTime, body) {
4159
+ this.#enter();
4160
+ let x;
4161
+ try {
4162
+ const requestId = this.#requestId(request);
4163
+ const ctx = this.#context(request, requestId, url.path, url.query, route, params, body);
4164
+ if (this.draining && target.kind === "route" && target.plan.definition.sse !== void 0) ctx.abort("shutdown");
4165
+ x = new Exchange(request, ctx, requestId, target, startTime);
4166
+ } catch (error) {
4167
+ this.#leave();
4168
+ return Promise.reject(error);
4169
+ }
4170
+ this.#container.runInScope(() => this.#respond(x, void 0), [[HttpTokens.Context, x.ctx], [HttpTokens.RequestId, x.requestId]]).then(() => this.#over(x), (error) => {
4171
+ this.#over(x);
4172
+ this.#logger.error("Failed to dispose request-scoped providers", {
4173
+ err: error,
4174
+ requestId: x.requestId
4175
+ });
3711
4176
  });
3712
- if (this.draining && target.kind === "route" && target.plan.definition.sse !== void 0) ctx.abort("shutdown");
3713
- const event = {
3714
- method: request.method,
3715
- path: ctx.path,
3716
- route: route?.path,
3717
- requestId,
3718
- headers: request.headers,
3719
- context: ctx,
3720
- startTime,
3721
- responseHeaders: {}
3722
- };
3723
- let outcome;
3724
- let reply;
3725
- let cors;
3726
- const preflight = target.kind === "preflight";
3727
- let replied = () => void 0;
3728
- const ready = new Promise((resolve) => {
3729
- replied = resolve;
4177
+ return this.#hooks.hasListeners("http:request") ? this.#emitted(x) : x.replied;
4178
+ }
4179
+ /** The reply of a request once the `http:request` listeners saw its event. */
4180
+ #emitted(x) {
4181
+ return x.replied.then((reply) => {
4182
+ this.#hooks.emit("http:request", x.event);
4183
+ return reply;
3730
4184
  });
3731
- const done = this.#container.runInScope(async () => {
3732
- let hookError;
3733
- let failed = false;
3734
- let run;
3735
- if (this.#options.cors !== void 0) cors = await this.#corsDecision(request, requestId);
3736
- try {
4185
+ }
4186
+ /** The request context of a request (internal). */
4187
+ #context(request, requestId, path, query, route, params, body) {
4188
+ const proxy = this.#options.trustProxy;
4189
+ 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));
4190
+ }
4191
+ /**
4192
+ * Answers a request in its scope: its outcome (`process`, or the target of the exchange after the
4193
+ * CORS decision and through the `http:request` wrappers), checked, completed and handed to the
4194
+ * adapter; a streamed body is then tracked until it was sent or abandoned. Never rejects.
4195
+ * `process` is given by the fast lane, which decided at the start of the request that no wrapper
4196
+ * applies and that CORS is off.
4197
+ */
4198
+ async #respond(x, process) {
4199
+ const ctx = x.ctx;
4200
+ const requestId = x.requestId;
4201
+ const event = x.event;
4202
+ let outcome;
4203
+ let hookError;
4204
+ let failed = false;
4205
+ let run;
4206
+ let recheck = false;
4207
+ let work = process;
4208
+ if (work === void 0) {
4209
+ if (this.#options.cors !== void 0) x.cors = await this.#corsDecision(x.request, requestId);
4210
+ recheck = this.#hooks.hasWrappers("http:request");
4211
+ const target = x.target;
4212
+ work = () => this.#process(ctx, target, event, x.cors);
4213
+ }
4214
+ try {
4215
+ if (recheck) {
4216
+ const inner = work;
3737
4217
  await this.#hooks.run("http:request", event, async () => {
3738
- outcome = await this.#process(ctx, target, event, cors);
3739
- if (outcome !== RAW) outcome = this.#checkCookies(this.#checkHeaders(outcome, ctx, event), ctx, event);
3740
- run = captureContext();
4218
+ outcome = this.#checked(await inner(), ctx, event);
4219
+ if (outcome !== RAW && isStreamed(outcome.body)) run = captureContext();
3741
4220
  return outcome;
3742
4221
  });
3743
- } catch (error) {
3744
- failed = true;
3745
- hookError = error;
3746
- }
3747
- if (outcome === void 0) {
3748
- const error = failed ? hookError : /* @__PURE__ */ new Error("An \"http:request\" hook wrapper did not call next()");
3749
- event.error ??= error;
3750
- const fallback = await this.#errorReply(error, ctx);
3751
- outcome = this.#checkCookies(this.#checkHeaders(fallback, ctx, event), ctx, event);
3752
- event.status = outcome.status;
3753
- } else if (failed) this.#logger.error("An \"http:request\" hook wrapper failed after the request was handled", {
3754
- err: hookError,
3755
- requestId
3756
- });
3757
- if (outcome === RAW) {
3758
- ctx.commit();
3759
- replied();
3760
- return;
3761
- }
3762
- const serialized = this.#complete(outcome, ctx, event, requestId, cors, preflight);
3763
- const body = serialized.body;
3764
- if (!(body instanceof ReadableStream || body instanceof Blob)) {
3765
- reply = adapterReply(serialized, body);
3766
- ctx.commit();
3767
- replied();
3768
- ctx.releaseBody();
3769
- return;
3770
- }
3771
- const tracked = trackStream(body, run ?? captureContext(), () => ctx.abort("client"));
3772
- const session = serialized.sse;
3773
- reply = adapterReply(serialized, tracked.stream, body instanceof Blob ? body : void 0);
3774
- if (session !== void 0) {
3775
- this.#streams.add(session);
3776
- if (this.draining) session.close("shutdown");
3777
- }
3778
- ctx.commit();
3779
- replied();
3780
- const failure = await tracked.done;
3781
- if (session !== void 0) this.#streams.delete(session);
3782
- if (failure !== void 0) this.#logger.error("The response stream failed", {
3783
- err: failure,
3784
- requestId,
3785
- path: ctx.path
3786
- });
3787
- ctx.releaseBody();
3788
- }, [[HttpTokens.Context, ctx], [HttpTokens.RequestId, requestId]]).then(() => {
3789
- ctx.complete();
3790
- }, (error) => {
3791
- ctx.complete();
3792
- this.#logger.error("Failed to dispose request-scoped providers", {
3793
- err: error,
3794
- requestId
3795
- });
4222
+ } else outcome = this.#checked(await work(), ctx, event);
4223
+ } catch (error) {
4224
+ failed = true;
4225
+ hookError = error;
4226
+ }
4227
+ if (outcome === void 0) {
4228
+ const error = failed ? hookError : /* @__PURE__ */ new Error("An \"http:request\" hook wrapper did not call next()");
4229
+ event.error ??= error;
4230
+ const fallback = await this.#errorReply(error, ctx);
4231
+ outcome = this.#checkCookies(this.#checkHeaders(fallback, ctx, event), ctx, event);
4232
+ event.status = outcome.status;
4233
+ recheck = true;
4234
+ } else if (failed) this.#logger.error("An \"http:request\" hook wrapper failed after the request was handled", {
4235
+ err: hookError,
4236
+ requestId
3796
4237
  });
3797
- await Promise.race([ready, done]);
3798
4238
  if (outcome === RAW) {
3799
- const status = request.rawStatus?.() ?? 200;
4239
+ ctx.commit();
4240
+ const status = x.request.rawStatus?.() ?? 200;
3800
4241
  event.status ??= status;
3801
- this.#emit(event);
3802
- return {
3803
- reply: {
3804
- status,
3805
- headers: {},
3806
- body: null,
3807
- raw: true
3808
- },
3809
- done
3810
- };
4242
+ x.answer({
4243
+ status,
4244
+ headers: {},
4245
+ body: null,
4246
+ raw: true
4247
+ });
4248
+ return;
3811
4249
  }
3812
- if (reply === void 0) {
3813
- const failure = /* @__PURE__ */ new Error("Request not handled");
3814
- reply = this.#complete(serializeError(500, this.#internalBody(failure), ctx.method), ctx, event, requestId, cors, preflight);
3815
- event.status = 500;
4250
+ const serialized = this.#complete(outcome, ctx, event, requestId, x.cors, x.preflight, recheck);
4251
+ const body = serialized.body;
4252
+ if (!isStreamed(body)) {
4253
+ const reply = adapterReply(serialized, body);
4254
+ ctx.commit();
4255
+ ctx.releaseBody();
4256
+ x.answer(reply);
4257
+ return;
4258
+ }
4259
+ const tracked = trackStream(body, run ?? captureContext(), () => ctx.abort("client"));
4260
+ const session = serialized.sse;
4261
+ const reply = adapterReply(serialized, tracked.stream, body instanceof Blob ? body : void 0);
4262
+ if (session !== void 0) {
4263
+ this.#streams.add(session);
4264
+ if (this.draining) session.close("shutdown");
4265
+ }
4266
+ ctx.commit();
4267
+ x.answer(reply);
4268
+ const failure = await tracked.done;
4269
+ if (session !== void 0) this.#streams.delete(session);
4270
+ if (failure !== void 0) this.#logger.error("The response stream failed", {
4271
+ err: failure,
4272
+ requestId,
4273
+ path: ctx.path
4274
+ });
4275
+ ctx.releaseBody();
4276
+ }
4277
+ /** The request is over (response body sent or abandoned, scope closed). */
4278
+ #over(x) {
4279
+ const ctx = x.ctx;
4280
+ ctx.complete();
4281
+ try {
4282
+ if (!x.answered) {
4283
+ const failure = /* @__PURE__ */ new Error("Request not handled");
4284
+ const reply = serializeError(500, this.#internalBody(failure), ctx.method);
4285
+ const completed = this.#complete(reply, ctx, x.event, x.requestId, x.cors, x.preflight, true);
4286
+ x.event.status = 500;
4287
+ x.answer(completed);
4288
+ }
4289
+ } catch (error) {
4290
+ x.fail(error);
4291
+ } finally {
4292
+ this.#leave();
3816
4293
  }
3817
- this.#emit(event);
3818
- return {
3819
- reply,
3820
- done
3821
- };
3822
4294
  }
3823
4295
  /**
3824
4296
  * Adds the headers of the `http:request` wrappers, then the framework headers (see
3825
- * {@link #frameworkHeaders}), then checks the final headers (see {@link #checkHeaders}).
4297
+ * {@link #frameworkHeaders}). When wrappers ran (`recheck`), checks the final headers again (see
4298
+ * {@link #checkHeaders}): the reply was checked before, and the cookies and framework headers are
4299
+ * valid by construction.
3826
4300
  */
3827
- #complete(reply, ctx, event, requestId, cors, preflight) {
4301
+ #complete(reply, ctx, event, requestId, cors, preflight, recheck) {
3828
4302
  addMissingHeaders(reply.headers, event.responseHeaders);
3829
4303
  applyCookies(reply, ctx);
3830
4304
  this.#frameworkHeaders(reply.headers, requestId, cors, preflight);
4305
+ if (!recheck) return reply;
3831
4306
  const checked = this.#checkHeaders(reply, ctx, event);
3832
4307
  if (checked !== reply) {
3833
4308
  applyCookies(checked, ctx);
@@ -3835,6 +4310,12 @@ var Pipeline = class {
3835
4310
  }
3836
4311
  return checked;
3837
4312
  }
4313
+ /** Checks the headers (unless only the serialiser set them), then the cookies of a reply. */
4314
+ #checked(outcome, ctx, event) {
4315
+ if (outcome === RAW) return outcome;
4316
+ const checked = outcome.ownHeaders === true ? outcome : this.#checkHeaders(outcome, ctx, event);
4317
+ return this.#checkCookies(checked, ctx, event);
4318
+ }
3838
4319
  /**
3839
4320
  * Serialises the cookies of an `HttpResponse` (`withCookie`) with the plugin settings (defaults,
3840
4321
  * signing). A cookie breaking a rule that depends on them (a prefix with the `secure` default,
@@ -3844,7 +4325,7 @@ var Pipeline = class {
3844
4325
  #checkCookies(reply, ctx, event) {
3845
4326
  const cookies = reply.cookies;
3846
4327
  if (cookies === void 0 || cookies.length === 0) return reply;
3847
- const settings = this.#options.cookies ?? DEFAULT_COOKIE_SETTINGS;
4328
+ const settings = this.#cookies;
3848
4329
  const lines = [];
3849
4330
  try {
3850
4331
  for (const cookie of cookies) lines.push(serializeWith(cookie, settings));
@@ -3869,7 +4350,7 @@ var Pipeline = class {
3869
4350
  */
3870
4351
  #frameworkHeaders(headers, requestId, cors, preflight) {
3871
4352
  const header = this.#options.requestIdHeader;
3872
- if (header !== void 0 && headers[header] === void 0 && isHeaderValue(requestId)) headers[header] = requestId;
4353
+ if (header !== void 0 && headers[header] === void 0 && (this.#validIds || isHeaderValue(requestId))) headers[header] = requestId;
3873
4354
  const policy = this.#options.cors;
3874
4355
  if (policy !== void 0) {
3875
4356
  if (!preflight) {
@@ -3878,8 +4359,7 @@ var Pipeline = class {
3878
4359
  }
3879
4360
  if (policy.varyOrigin) mergeVary(headers, "origin");
3880
4361
  }
3881
- const security = this.#options.securityHeaders;
3882
- if (security !== void 0) addMissingHeaders(headers, security);
4362
+ for (const [name, value] of this.#securityHeaders) if (headers[name] === void 0) headers[name] = value;
3883
4363
  if (this.draining) headers["connection"] = "close";
3884
4364
  }
3885
4365
  /**
@@ -3921,7 +4401,12 @@ var Pipeline = class {
3921
4401
  this.#log(error, ctx);
3922
4402
  return serializeError(500, this.#internalBody(error), ctx.method);
3923
4403
  }
3924
- async #process(ctx, target, event, cors) {
4404
+ /** The outcome of a request: its route run, or a reply of the framework. Never rejects (errors are mapped). */
4405
+ #process(ctx, target, event, cors) {
4406
+ return target.kind === "route" ? this.#invoke(ctx, target.plan, event) : this.#answer(ctx, target, event, cors);
4407
+ }
4408
+ /** Answers a request without running a route. Never rejects. */
4409
+ async #answer(ctx, target, event, cors) {
3925
4410
  try {
3926
4411
  switch (target.kind) {
3927
4412
  case "error": throw target.error;
@@ -3944,30 +4429,71 @@ var Pipeline = class {
3944
4429
  status: 308,
3945
4430
  headers: { location: target.location }
3946
4431
  }));
3947
- default: return await this.#invoke(ctx, target.plan, event);
3948
4432
  }
3949
4433
  } catch (error) {
3950
- event.error = error;
3951
- const reply = await this.#errorReply(error, ctx);
3952
- event.status = reply.status;
3953
- return reply;
4434
+ return await this.#failed(error, ctx, event);
3954
4435
  }
3955
4436
  }
4437
+ /** The reply of an error (see {@link #errorReply}), recorded on the event. */
4438
+ async #failed(error, ctx, event) {
4439
+ event.error = error;
4440
+ const reply = await this.#errorReply(error, ctx);
4441
+ event.status = reply.status;
4442
+ return reply;
4443
+ }
4444
+ /** Runs a route: guards, body, interceptors, handler, serialisation. Never rejects (errors are mapped). */
3956
4445
  async #invoke(ctx, plan, event) {
3957
- const controller = this.#resolve(plan.controller);
3958
- for (const guard of plan.guards) if (await this.#resolve(guard).canActivate(ctx) !== true) throw new ForbiddenException("Forbidden resource");
3959
- if (plan.parseBody) await ctx.parseBody();
3960
- const definition = plan.definition;
3961
- const interceptors = plan.interceptors;
3962
4446
  let result;
3963
- if (interceptors.length === 0) result = await definition.invoke(controller, ctx);
3964
- else {
3965
- const chain = async (index) => {
3966
- if (index === interceptors.length) return await definition.invoke(controller, ctx);
3967
- return await this.#resolve(interceptors[index]).intercept(ctx, () => chain(index + 1));
3968
- };
3969
- result = await chain(0);
4447
+ try {
4448
+ const controller = this.#resolve(plan.controller);
4449
+ for (const guard of plan.guards) if (await this.#resolve(guard).canActivate(ctx) !== true) throw new ForbiddenException("Forbidden resource");
4450
+ if (plan.parseBody) await ctx.parseBody();
4451
+ const definition = plan.definition;
4452
+ const interceptors = plan.interceptors;
4453
+ if (interceptors.length === 0) result = definition.invoke(controller, ctx);
4454
+ else {
4455
+ const chain = async (index) => {
4456
+ if (index === interceptors.length) return await definition.invoke(controller, ctx);
4457
+ return await this.#resolve(interceptors[index]).intercept(ctx, () => chain(index + 1));
4458
+ };
4459
+ result = chain(0);
4460
+ }
4461
+ } catch (error) {
4462
+ return await this.#failed(error, ctx, event);
3970
4463
  }
4464
+ const settled = this.#settle(ctx, plan, event, result);
4465
+ return settled instanceof Promise ? await settled : settled;
4466
+ }
4467
+ /**
4468
+ * The rest of {@link #invoke} once the handler (and its interceptors) returned `result`: awaited
4469
+ * when it is a thenable, then serialised. Never throws nor rejects (errors are mapped).
4470
+ */
4471
+ #settle(ctx, plan, event, result) {
4472
+ if (isThenable(result)) return this.#settleLater(ctx, plan, event, result);
4473
+ try {
4474
+ const outcome = this.#outcome(ctx, plan, event, result);
4475
+ return outcome instanceof Promise ? outcome.catch((error) => this.#failed(error, ctx, event)) : outcome;
4476
+ } catch (error) {
4477
+ return this.#failed(error, ctx, event);
4478
+ }
4479
+ }
4480
+ async #settleLater(ctx, plan, event, pending) {
4481
+ let result;
4482
+ try {
4483
+ result = await pending;
4484
+ } catch (error) {
4485
+ return await this.#failed(error, ctx, event);
4486
+ }
4487
+ const settled = this.#settle(ctx, plan, event, result);
4488
+ return settled instanceof Promise ? await settled : settled;
4489
+ }
4490
+ /**
4491
+ * The outcome of a handler result: `RAW` for a `@Res()` route, otherwise its reply (files resolved,
4492
+ * event streams opened: only an `HttpResponse` may need an asynchronous step). Throws (or rejects)
4493
+ * when the result cannot be served.
4494
+ */
4495
+ #outcome(ctx, plan, event, result) {
4496
+ const definition = plan.definition;
3971
4497
  if (definition.rawResponse === true && !(result instanceof Response)) return RAW;
3972
4498
  const options = {
3973
4499
  method: ctx.method,
@@ -3977,12 +4503,12 @@ var Pipeline = class {
3977
4503
  context: ctx.hasResponseHeaders ? ctx.responseHeaders : void 0
3978
4504
  };
3979
4505
  const sse = definition.sse;
3980
- let reply;
3981
4506
  if (sse !== void 0 && !(result instanceof Response) && !isHttpResponse(result)) {
3982
4507
  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)})`);
3983
- reply = serializeSseReply(this.#openStream(result, sseOptions(sse), ctx), void 0, options);
3984
- } else reply = await this.#serialize(result, ctx, options, sse);
3985
- return this.#finish(event, reply);
4508
+ return this.#finish(event, serializeSseReply(this.#openStream(result, sseOptions(sse), ctx), void 0, options));
4509
+ }
4510
+ if (!isHttpResponse(result)) return this.#finish(event, serializeResult(result, options));
4511
+ return this.#serialize(result, ctx, options, sse).then((reply) => this.#finish(event, reply));
3986
4512
  }
3987
4513
  /** Serialises a handler (or exception mapper) result; file bodies are resolved, event streams opened. */
3988
4514
  async #serialize(result, ctx, options, route) {
@@ -4132,17 +4658,9 @@ var Pipeline = class {
4132
4658
  }
4133
4659
  return this.#options.generateRequestId();
4134
4660
  }
4135
- #emit(event) {
4136
- if (this.#hooks.hasListeners("http:request")) this.#hooks.emit("http:request", event);
4137
- }
4138
4661
  #enter() {
4139
4662
  this.#inflight++;
4140
4663
  }
4141
- /** Leaves once the request is over (immediately when it failed before being served). */
4142
- #leaveWhen(served) {
4143
- if (served === void 0) this.#leave();
4144
- else served.done.then(() => this.#leave());
4145
- }
4146
4664
  #leave() {
4147
4665
  this.#inflight--;
4148
4666
  if (this.#inflight === 0 && this.#idle.length > 0) {
@@ -4152,6 +4670,126 @@ var Pipeline = class {
4152
4670
  }
4153
4671
  }
4154
4672
  };
4673
+ /** `#fastAsync` reads the body and runs the handler. */
4674
+ const NO_RESULT = Symbol("no result");
4675
+ /** A request being answered (internal): its context, its target and the promise of its reply. */
4676
+ var Exchange = class {
4677
+ request;
4678
+ ctx;
4679
+ requestId;
4680
+ target;
4681
+ preflight;
4682
+ replied;
4683
+ cors = void 0;
4684
+ answered = false;
4685
+ /** The `http:request` listeners will see the event (fast lane: keep its status). */
4686
+ tracked = false;
4687
+ /** The asynchronous part of a fast-lane request (never rejects). */
4688
+ done = void 0;
4689
+ #resolve;
4690
+ #reject;
4691
+ #event;
4692
+ /** `startTime`: the event is created now (general pipeline); otherwise when first needed (fast lane). */
4693
+ constructor(request, ctx, requestId, target, startTime) {
4694
+ this.request = request;
4695
+ this.ctx = ctx;
4696
+ this.requestId = requestId;
4697
+ this.target = target;
4698
+ this.preflight = target.kind === "preflight";
4699
+ let resolve;
4700
+ let reject;
4701
+ this.replied = new Promise((onReply, onError) => {
4702
+ resolve = onReply;
4703
+ reject = onError;
4704
+ });
4705
+ this.#resolve = resolve;
4706
+ this.#reject = reject;
4707
+ if (startTime !== void 0) this.#event = requestEvent(request, ctx, startTime);
4708
+ }
4709
+ /**
4710
+ * The `http:request` event. The fast lane creates it when the general pipeline takes over: no
4711
+ * hook saw the request start, so its `startTime` is not observed.
4712
+ */
4713
+ get event() {
4714
+ this.#event ??= requestEvent(this.request, this.ctx, performance.now());
4715
+ return this.#event;
4716
+ }
4717
+ answer(reply) {
4718
+ this.answered = true;
4719
+ this.#resolve(reply);
4720
+ }
4721
+ fail(error) {
4722
+ this.#reject(error);
4723
+ }
4724
+ };
4725
+ /** The `http:request` event of a request. */
4726
+ function requestEvent(request, ctx, startTime) {
4727
+ return {
4728
+ method: request.method,
4729
+ path: ctx.path,
4730
+ route: ctx.route?.path,
4731
+ requestId: ctx.requestId,
4732
+ headers: request.headers,
4733
+ context: ctx,
4734
+ startTime,
4735
+ responseHeaders: {}
4736
+ };
4737
+ }
4738
+ /**
4739
+ * The decoded parameters of a raw request path matched against a route, in one pass: what
4740
+ * `matchRoutePath` then `decodePathValue` (see `Pipeline.#route`) give for a path they accept.
4741
+ * `undefined` when the path does not match or a value is not valid percent-encoding: the general
4742
+ * pipeline then answers it (a reroute, a 400).
4743
+ */
4744
+ function routeParams(route, path) {
4745
+ if (path.charCodeAt(0) !== 47) return void 0;
4746
+ const params = emptyRecord();
4747
+ const segments = route.segments;
4748
+ const last = segments.length - 1;
4749
+ if (last === -1) return path === "/" ? params : void 0;
4750
+ let position = 1;
4751
+ for (let index = 0; index <= last; index++) {
4752
+ const segment = segments[index];
4753
+ if (segment.kind === "wildcard") {
4754
+ const rest = decodePathValue(path.slice(position));
4755
+ if (rest === void 0) return void 0;
4756
+ params["*"] = rest;
4757
+ return params;
4758
+ }
4759
+ const slash = path.indexOf("/", position);
4760
+ const end = slash === -1 ? path.length : slash;
4761
+ if (segment.kind === "static") {
4762
+ const value = segment.value;
4763
+ if (end - position !== value.length || !path.startsWith(value, position)) return void 0;
4764
+ } else {
4765
+ if (end === position) return void 0;
4766
+ const decoded = decodePathValue(path.slice(position, end));
4767
+ if (decoded === void 0) return void 0;
4768
+ params[segment.name] = decoded;
4769
+ }
4770
+ if (index === last) return end === path.length ? params : void 0;
4771
+ if (slash === -1) return void 0;
4772
+ position = slash + 1;
4773
+ }
4774
+ return params;
4775
+ }
4776
+ /**
4777
+ * A value the fast lane serialises as JSON: a plain object or array (its own prototype and
4778
+ * constructor), not duck-typed as a Node.js stream. Anything `serializeResult` could treat otherwise
4779
+ * (`HttpResponse`, `Response`, bytes, streams, `Blob`, strings, `undefined`...) is not.
4780
+ */
4781
+ function isPlainJson(value) {
4782
+ if (value === null || typeof value !== "object") return false;
4783
+ const prototype = Object.getPrototypeOf(value);
4784
+ const ctor = value.constructor;
4785
+ if (prototype === Object.prototype) {
4786
+ if (ctor !== Object) return false;
4787
+ } else if (prototype !== Array.prototype || ctor !== Array) return false;
4788
+ return typeof value.pipe !== "function";
4789
+ }
4790
+ const ENCODER = new TextEncoder();
4791
+ /** UTF-8 encoding of a text body, like the serialiser (a pooled `Buffer` on Node.js, `TextEncoder` on Bun). */
4792
+ const utf8 = isBun() ? (text) => ENCODER.encode(text) : (text) => Buffer$1.from(text, "utf8");
4155
4793
  /**
4156
4794
  * The reply handed to the adapter (internal fields of the serialised reply left out). A streamed
4157
4795
  * `text/event-stream` body is an event stream whatever produced it (`@Sse`, `HttpResponse.sse()`, a
@@ -4159,6 +4797,13 @@ var Pipeline = class {
4159
4797
  * before a quiet stream's first byte, and lifts idle timeouts.
4160
4798
  */
4161
4799
  function adapterReply(serialized, body, blob) {
4800
+ const text = serialized.text;
4801
+ if (text !== void 0) return {
4802
+ status: serialized.status,
4803
+ headers: serialized.headers,
4804
+ body,
4805
+ text
4806
+ };
4162
4807
  const reply = {
4163
4808
  status: serialized.status,
4164
4809
  headers: serialized.headers,
@@ -4180,7 +4825,7 @@ function adapterReply(serialized, body, blob) {
4180
4825
  * one in place. One line per cookie.
4181
4826
  */
4182
4827
  function applyCookies(reply, ctx) {
4183
- const own = reply.cookieLines ?? [];
4828
+ const own = reply.cookieLines ?? NO_COOKIE_LINES;
4184
4829
  const context = ctx.cookieLines;
4185
4830
  if (own.length === 0 && context.length === 0) return;
4186
4831
  const merged = /* @__PURE__ */ new Map();
@@ -4192,6 +4837,11 @@ function applyCookies(reply, ctx) {
4192
4837
  else if (existing !== void 0) raw = [...existing];
4193
4838
  reply.headers["set-cookie"] = [...raw, ...merged.values()];
4194
4839
  }
4840
+ const NO_COOKIE_LINES = Object.freeze([]);
4841
+ /** Whether a reply body is streamed (a file is read through a stream too). */
4842
+ function isStreamed(body) {
4843
+ return body instanceof ReadableStream || body instanceof Blob;
4844
+ }
4195
4845
  /** `SseOptions` of the `sse` metadata of a route (`heartbeat: 0` disables the heartbeat). */
4196
4846
  function sseOptions(definition) {
4197
4847
  return {
@@ -4925,7 +5575,7 @@ var HttpPlugin = class HttpPlugin {
4925
5575
  adapter.registerRoute({
4926
5576
  method: registration.method,
4927
5577
  path: registration.path,
4928
- handler: (request) => pipeline.handle(plan, request)
5578
+ handler: pipeline.handler(plan)
4929
5579
  });
4930
5580
  routes.push(Object.freeze({
4931
5581
  method: registration.method,
@@ -5029,7 +5679,7 @@ function resolve$1(options) {
5029
5679
  if (typeof drainTimeoutMs !== "number" || !(drainTimeoutMs >= 0)) throw invalidOption("drainTimeoutMs", "a positive number (or Infinity)", drainTimeoutMs);
5030
5680
  const requestIdHeader = options.requestIdHeader ?? DEFAULT_REQUEST_ID_HEADER;
5031
5681
  if (requestIdHeader !== false && (typeof requestIdHeader !== "string" || !isHeaderName(requestIdHeader))) throw invalidOption("requestIdHeader", "a header name or false", requestIdHeader);
5032
- const generateRequestId = options.generateRequestId ?? (() => crypto.randomUUID());
5682
+ const generateRequestId = options.generateRequestId ?? randomRequestId;
5033
5683
  if (typeof generateRequestId !== "function") throw invalidOption("generateRequestId", "a function", generateRequestId);
5034
5684
  const adapter = options.adapter ?? nodeAdapter();
5035
5685
  if (adapter === null || typeof adapter !== "object" || typeof adapter.registerRoute !== "function" || typeof adapter.listen !== "function" || typeof adapter.inject !== "function") throw invalidOption("adapter", "an HttpAdapter (nodeAdapter(), fastifyAdapter()...)", adapter);
@@ -5577,7 +6227,8 @@ function isStandardSchema(value) {
5577
6227
  * the schema strips them).
5578
6228
  *
5579
6229
  * Returns synchronously when the schema validates synchronously, a promise otherwise (async
5580
- * refinements): generated code always `await`s it.
6230
+ * refinements): generated code calls the handler directly with the value, and `await`s the value
6231
+ * when it is a thenable (`isThenable`).
5581
6232
  *
5582
6233
  * @param location what is validated, used in the error message: `body`, `query`, `query "page"`,
5583
6234
  * `param "id"`, `headers`, `cookie "sid"`, `cookies`...
@@ -5587,8 +6238,9 @@ function isStandardSchema(value) {
5587
6238
  * @throws TypeError when `schema` is not a Standard Schema v1 (a programming error → 500).
5588
6239
  */
5589
6240
  function validate(schema, value, location) {
5590
- if (!isStandardSchema(schema)) throw new TypeError(`The schema of ${location} is not a Standard Schema v1 (missing "~standard.validate")`);
5591
- const result = schema["~standard"].validate(value);
6241
+ const props = schema !== null && (typeof schema === "object" || typeof schema === "function") ? schema["~standard"] : void 0;
6242
+ 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")`);
6243
+ const result = props.validate(value);
5592
6244
  if (isThenable(result)) return Promise.resolve(result).then((settled) => unwrap(settled, location));
5593
6245
  return unwrap(result, location);
5594
6246
  }
@@ -5613,6 +6265,6 @@ function redact(issue) {
5613
6265
  path
5614
6266
  };
5615
6267
  }
5616
- 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 };
6268
+ 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 };
5617
6269
 
5618
6270
  //# sourceMappingURL=index.mjs.map