@znight/http 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -654,14 +654,31 @@ interface RouteMetaEntry<T = unknown> {
654
654
  /** Default `false`. */
655
655
  readonly required?: boolean;
656
656
  }
657
+ /**
658
+ * Anything that carries effective route metadata: the {@link HttpContext} of a request, and the
659
+ * contexts of other transports that reuse route decorators (a GraphQL resolver context, a WebSocket
660
+ * gateway context). {@link RouteDecorator.get} reads `route.meta[key]`; nothing else is required.
661
+ *
662
+ * ```ts
663
+ * const carrier: RouteMetaCarrier = { route: { meta: { roles: ['admin'] } } }
664
+ * Roles.get(carrier) // ['admin']
665
+ * ```
666
+ */
667
+ interface RouteMetaCarrier {
668
+ /** The matched route (or field, or handler), `undefined` when there is none. */
669
+ readonly route?: {
670
+ readonly meta: Readonly<Record<string, unknown>>;
671
+ } | undefined;
672
+ }
657
673
  /** A user-defined route metadata decorator created with {@link createRouteDecorator}. */
658
674
  interface RouteDecorator<T> extends RouteDecoratorInfo {
659
675
  (...args: RouteDecoratorArgs<T>): ClassMarker & MethodMarker;
660
676
  /**
661
677
  * The value of the current route, if set. Reads the effective metadata: the route's decision of
662
- * a group wins over the controller's.
678
+ * a group wins over the controller's. Accepts any {@link RouteMetaCarrier}: an
679
+ * {@link HttpContext}, or the context of another transport exposing `route.meta`.
663
680
  */
664
- get(ctx: HttpContext): T | undefined;
681
+ get(ctx: RouteMetaCarrier): T | undefined;
665
682
  /**
666
683
  * The same metadata as data (`{ key, value, group, required }`), for routes that are not compiled
667
684
  * controllers (plugin routes: `OpenApiPlugin.configure({ meta: [Public.meta()] })`). The value is
@@ -1113,6 +1130,13 @@ interface PluginRoute {
1113
1130
  readonly meta?: readonly RouteMetaEntry[];
1114
1131
  /** Guards of the route (provider classes or instances), run after the global guards. */
1115
1132
  readonly guards?: readonly GuardRef[];
1133
+ /**
1134
+ * Maximum body size of this route (`'100kb'`, `1048576`, `Infinity`), with the grammar of the
1135
+ * `HttpPlugin` option `bodyLimit`, which it replaces for this route only (lower or higher, like
1136
+ * `@BodyLimit` on a controller route). Past it, reading the body answers 413. Default: the
1137
+ * `HttpPlugin` option.
1138
+ */
1139
+ readonly bodyLimit?: ByteSize;
1116
1140
  }
1117
1141
  /** What {@link PluginHttpRoutes.httpRoutes} receives. */
1118
1142
  interface PluginHttpRoutesContext {
@@ -1258,5 +1282,5 @@ export declare function isThenable<T>(value: T): value is unknown extends T ? T
1258
1282
  * @throws TypeError when `schema` is not a Standard Schema v1 (a programming error → 500).
1259
1283
  */
1260
1284
  export declare function validate<S extends StandardSchemaV1>(schema: S, value: unknown, location: string): InferSchemaOutput<S> | Promise<InferSchemaOutput<S>>;
1261
- export { type AdapterCloseOptions, type AdapterHandler, type AdapterReply, type AdapterRequest, type AdapterRoute, BadGatewayException, BadRequestException, type ByteRange, type ByteSize, type CallHandler, type CanActivate, type ClearCookieOptions, type Coercer, type Coercers, ConflictException, type ContentDispositionType, type ControllerOptions, type CookieDefaults, type CookieOptions, type CorsOptions, type CorsOrigin, type EnforcementMode, type EventChannelOptions, type ExceptionMapper, type ExceptionMapperClass, type ExceptionMapperRef, type ExceptionMapperResult, type FallbackHandler, type FileBody, type FileResponseOptions, ForbiddenException, GatewayTimeoutException, GoneException, type GuardClass, type GuardEnforcement, type GuardRef, type HeaderValue, type HstsOptions, type HttpAdapter, type HttpContext, type HttpControllerDefinition, type HttpCookieOptions, type HttpErrorBody, HttpException, type HttpExceptionOptions, type HttpInterceptor, type HttpInterceptorClass, type HttpMetadata, type HttpMethod, type HttpParamDefinition, type HttpParamSource, type HttpPluginOptions, type HttpResponseInit, type HttpRouteDefinition, type HttpRouteGroupDefinition, type HttpRouteInfo, type HttpRouteSummary, type HttpServerAddress, type HttpSseDefinition, type HttpSseOptions, type HttpUploadDefinition, type InferSchemaInput, type InferSchemaOutput, type InjectRequest, type InjectResponse, type InjectSseResponse, type InjectSseWaitOptions, type InjectStreamOptions, type InjectStreamRequest, type InjectStreamResponse, type InterceptorRef, InternalServerErrorException, type MediaType, MethodNotAllowedException, type NodeAdapter, type NodeAdapterOptions, NotFoundException, NotImplementedException, type ParseSseOptions, PayloadTooLargeException, type PluginHttpRoutes, type PluginHttpRoutesContext, type PluginRoute, type QueryRecord, type RedirectStatus, type ReplyBody, type RequestAbortKind, RequestAbortedError, type RequestId, type ResponseCookie, type RouteDecorator, type RouteDecoratorArgs, type RouteDecoratorInfo, type RouteDecoratorOptions, type RouteMetaEntry, type SecurityHeadersOptions, ServiceUnavailableException, type SetCookieInfo, type SseBody, type SseEvent, type SseItem, type SseMessage, type SseOptions, type SseSource, type StandardSchemaV1, type StandardSchemaV1FailureResult, type StandardSchemaV1Issue, type StandardSchemaV1Options, type StandardSchemaV1PathSegment, type StandardSchemaV1Props, type StandardSchemaV1Result, type StandardSchemaV1SuccessResult, type StandardSchemaV1Types, type StreamResponseOptions, type StreamSource, TooManyRequestsException, type TrailingSlashPolicy, type TrustProxy, UnauthorizedException, UnprocessableEntityException, UnsupportedMediaTypeException, type UploadLimits, type UploadedFileOptions, type UploadedFilesOptions, type Uploads, ValidationException, type ValidationIssueDetail, formatByteSize, isHttpException, isStandardSchema, nodeAdapter, parseByteSize, parseCookieHeader, parseQuery, parseSetCookie, serializeClearCookie, serializeCookie, toIssueDetail, uploads };
1285
+ export { type AdapterCloseOptions, type AdapterHandler, type AdapterReply, type AdapterRequest, type AdapterRoute, BadGatewayException, BadRequestException, type ByteRange, type ByteSize, type CallHandler, type CanActivate, type ClearCookieOptions, type Coercer, type Coercers, ConflictException, type ContentDispositionType, type ControllerOptions, type CookieDefaults, type CookieOptions, type CorsOptions, type CorsOrigin, type EnforcementMode, type EventChannelOptions, type ExceptionMapper, type ExceptionMapperClass, type ExceptionMapperRef, type ExceptionMapperResult, type FallbackHandler, type FileBody, type FileResponseOptions, ForbiddenException, GatewayTimeoutException, GoneException, type GuardClass, type GuardEnforcement, type GuardRef, type HeaderValue, type HstsOptions, type HttpAdapter, type HttpContext, type HttpControllerDefinition, type HttpCookieOptions, type HttpErrorBody, HttpException, type HttpExceptionOptions, type HttpInterceptor, type HttpInterceptorClass, type HttpMetadata, type HttpMethod, type HttpParamDefinition, type HttpParamSource, type HttpPluginOptions, type HttpResponseInit, type HttpRouteDefinition, type HttpRouteGroupDefinition, type HttpRouteInfo, type HttpRouteSummary, type HttpServerAddress, type HttpSseDefinition, type HttpSseOptions, type HttpUploadDefinition, type InferSchemaInput, type InferSchemaOutput, type InjectRequest, type InjectResponse, type InjectSseResponse, type InjectSseWaitOptions, type InjectStreamOptions, type InjectStreamRequest, type InjectStreamResponse, type InterceptorRef, InternalServerErrorException, type MediaType, MethodNotAllowedException, type NodeAdapter, type NodeAdapterOptions, NotFoundException, NotImplementedException, type ParseSseOptions, PayloadTooLargeException, type PluginHttpRoutes, type PluginHttpRoutesContext, type PluginRoute, type QueryRecord, type RedirectStatus, type ReplyBody, type RequestAbortKind, RequestAbortedError, type RequestId, type ResponseCookie, type RouteDecorator, type RouteDecoratorArgs, type RouteDecoratorInfo, type RouteDecoratorOptions, type RouteMetaCarrier, type RouteMetaEntry, type SecurityHeadersOptions, ServiceUnavailableException, type SetCookieInfo, type SseBody, type SseEvent, type SseItem, type SseMessage, type SseOptions, type SseSource, type StandardSchemaV1, type StandardSchemaV1FailureResult, type StandardSchemaV1Issue, type StandardSchemaV1Options, type StandardSchemaV1PathSegment, type StandardSchemaV1Props, type StandardSchemaV1Result, type StandardSchemaV1SuccessResult, type StandardSchemaV1Types, type StreamResponseOptions, type StreamSource, TooManyRequestsException, type TrailingSlashPolicy, type TrustProxy, UnauthorizedException, UnprocessableEntityException, UnsupportedMediaTypeException, type UploadLimits, type UploadedFileOptions, type UploadedFilesOptions, type Uploads, ValidationException, type ValidationIssueDetail, formatByteSize, isHttpException, isStandardSchema, nodeAdapter, parseByteSize, parseCookieHeader, parseQuery, parseSetCookie, serializeClearCookie, serializeCookie, toIssueDetail, uploads };
1262
1286
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -3884,25 +3884,29 @@ var Pipeline = class {
3884
3884
  },
3885
3885
  method: definition.method,
3886
3886
  jsonStatus: definition.method === "HEAD" || isBodylessStatus(json) ? void 0 : json,
3887
- emptyStatus: definition.status ?? (post ? 201 : 204)
3887
+ emptyStatus: definition.status ?? (post ? 201 : 204),
3888
+ responses: definition.method !== "HEAD"
3888
3889
  };
3889
3890
  }
3890
3891
  /**
3891
3892
  * 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).
3893
+ * of per request, without promises while the route stays synchronous. A request takes it when the
3894
+ * server is not draining and its method is the route's (not a `HEAD` served by a `GET` route); a
3895
+ * plain object or array returned by the handler is serialised as JSON, and `undefined` answered
3896
+ * without body, with the headers in their final order (a template precomputed at boot).
3897
3897
  * A thenable result is awaited in the lane (see {@link #fastAsync}). Anything else — a malformed
3898
3898
  * path (before the request starts), a thrown error or a rejection, an `HttpResponse`, a `Response`
3899
3899
  * or any other result, headers or cookies set through the context — continues in the general
3900
3900
  * pipeline, in the same request scope, with the same outcome.
3901
+ * With `http:request` wrappers, the handler runs inside them (see {@link #fastWrapped}); listeners
3902
+ * get the event once the reply is ready, as in the general pipeline.
3901
3903
  */
3902
3904
  #fast(route, request) {
3903
3905
  const plan = route.plan;
3906
+ if (this.draining || request.method !== route.method) return this.handle(plan, request);
3904
3907
  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);
3908
+ const wrapped = hooks.hasWrappers("http:request");
3909
+ const startTime = wrapped || hooks.hasListeners("http:request") ? performance.now() : void 0;
3906
3910
  const url = request.url;
3907
3911
  const mark = url.indexOf("?");
3908
3912
  const path = mark === -1 ? url : url.slice(0, mark);
@@ -3918,13 +3922,13 @@ var Pipeline = class {
3918
3922
  return Promise.reject(error);
3919
3923
  }
3920
3924
  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);
3925
+ if (wrapped) {
3926
+ const x = new Exchange(request, ctx, ctx.requestId, route.target, startTime);
3927
+ x.done = scope.run(this.#fastWrapped, x, route);
3928
+ return this.#fastExchange(x, scope);
3927
3929
  }
3930
+ const done = scope.run(this.#fastRun, route, request, ctx, startTime);
3931
+ if (done instanceof Exchange) return this.#fastExchange(done, scope);
3928
3932
  const closing = scope.close();
3929
3933
  if (closing === void 0) {
3930
3934
  ctx.complete();
@@ -3935,7 +3939,7 @@ var Pipeline = class {
3935
3939
  closing.then(() => this.#over(x), (error) => this.#disposalFailed(x, error));
3936
3940
  }
3937
3941
  if (!hooks.hasListeners("http:request")) return Promise.resolve(done);
3938
- const event = requestEvent(request, ctx, performance.now());
3942
+ const event = requestEvent(request, ctx, startTime ?? performance.now());
3939
3943
  event.status = done.status;
3940
3944
  return Promise.resolve(done).then((reply) => {
3941
3945
  hooks.emit("http:request", event);
@@ -3943,20 +3947,31 @@ var Pipeline = class {
3943
3947
  });
3944
3948
  }
3945
3949
  /**
3950
+ * The reply of a fast-lane request whose asynchronous part runs in `x.done`: the request ends once
3951
+ * it settled (see {@link #end}); listeners get the event once the reply is ready.
3952
+ */
3953
+ #fastExchange(x, scope) {
3954
+ x.done.then(() => this.#end(x, scope, void 0), (error) => this.#end(x, scope, error));
3955
+ if (!this.#hooks.hasListeners("http:request")) return x.replied;
3956
+ x.tracked = true;
3957
+ return this.#emitted(x);
3958
+ }
3959
+ /**
3946
3960
  * The fast lane in the request scope: the reply when the handler answered synchronously with a
3947
3961
  * plain JSON value or `undefined`, otherwise the exchange finishing the request (see {@link #fast}).
3962
+ * `startTime`: the request has `http:request` listeners, its event starts with the exchange.
3948
3963
  */
3949
- #fastRun = (route, request, ctx) => {
3964
+ #fastRun = (route, request, ctx, startTime) => {
3950
3965
  const plan = route.plan;
3951
- if (plan.parseBody) return this.#fastLater(route, request, ctx, NO_RESULT);
3966
+ if (plan.parseBody) return this.#fastLater(route, request, ctx, NO_RESULT, startTime);
3952
3967
  let result;
3953
3968
  let reply;
3954
3969
  try {
3955
3970
  result = plan.definition.invoke(this.#resolve(plan.controller), ctx);
3956
- if (isThenable(result)) return this.#fastLater(route, request, ctx, result);
3971
+ if (isThenable(result)) return this.#fastLater(route, request, ctx, result, startTime);
3957
3972
  reply = this.#fastReply(route, ctx, result);
3958
3973
  } catch (error) {
3959
- const x = new Exchange(request, ctx, ctx.requestId, route.target);
3974
+ const x = new Exchange(request, ctx, ctx.requestId, route.target, startTime);
3960
3975
  x.done = this.#respond(x, () => this.#failed(error, ctx, x.event));
3961
3976
  return x;
3962
3977
  }
@@ -3965,13 +3980,13 @@ var Pipeline = class {
3965
3980
  ctx.releaseBody();
3966
3981
  return reply;
3967
3982
  }
3968
- const x = new Exchange(request, ctx, ctx.requestId, route.target);
3983
+ const x = new Exchange(request, ctx, ctx.requestId, route.target, startTime);
3969
3984
  x.done = this.#respond(x, () => this.#settle(ctx, plan, x.event, result));
3970
3985
  return x;
3971
3986
  };
3972
3987
  /** 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);
3988
+ #fastLater(route, request, ctx, result, startTime) {
3989
+ const x = new Exchange(request, ctx, ctx.requestId, route.target, startTime);
3975
3990
  x.done = this.#fastAsync(x, route, result);
3976
3991
  return x;
3977
3992
  }
@@ -4007,17 +4022,39 @@ var Pipeline = class {
4007
4022
  x.answer(reply);
4008
4023
  }
4009
4024
  /**
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`.
4025
+ * The reply of a plain JSON value (see {@link isPlainJson}), of an `HttpResponse` carrying one
4026
+ * (see {@link fastResponseType}) or of `undefined`: what `serializeResult` and {@link #complete}
4027
+ * produce for it, the headers in their final order. `undefined` when the result or the context
4028
+ * needs the general pipeline. Throws like `JSON.stringify`.
4014
4029
  */
4015
4030
  #fastReply(route, ctx, result) {
4016
- const status = result === void 0 ? route.emptyStatus : route.jsonStatus;
4031
+ let payload = result;
4032
+ let status = result === void 0 ? route.emptyStatus : route.jsonStatus;
4033
+ let type = null;
4034
+ if (route.responses && result instanceof HttpResponse) {
4035
+ const own = fastResponseType(result);
4036
+ if (own === void 0) return void 0;
4037
+ type = own;
4038
+ payload = result.body;
4039
+ if (route.plan.definition.status === void 0 && result.status !== void 0) status = isBodylessStatus(result.status) ? void 0 : result.status;
4040
+ if (payload === void 0) return void 0;
4041
+ }
4017
4042
  if (status === void 0 || ctx.hasResponseHeaders || ctx.hasCookies || this.draining) return void 0;
4018
4043
  const requestId = ctx.requestId;
4019
4044
  if (!this.#validIds && !isHeaderValue(requestId)) return void 0;
4020
- if (result === void 0) {
4045
+ if (payload === void 0) return this.#templateReply(status, null, void 0, requestId, null);
4046
+ if (!isPlainJson(payload)) return void 0;
4047
+ const json = JSON.stringify(payload);
4048
+ if (json === void 0) throw new TypeError(`Cannot serialise a ${typeof payload} as a response body`);
4049
+ return this.#templateReply(status, utf8(json), json, requestId, type);
4050
+ }
4051
+ /**
4052
+ * The reply of a JSON body (`text` set) or of no body, its headers copied from the template of
4053
+ * the fast lane (see {@link #jsonHeaders}), completed with the length and the request id (one that
4054
+ * can be sent). `type` replaces the JSON content type in place (the `HttpResponse`'s own).
4055
+ */
4056
+ #templateReply(status, body, text, requestId, type) {
4057
+ if (text === void 0) {
4021
4058
  const headers = { ...this.#emptyHeaders };
4022
4059
  const idHeader = this.#emptyIdHeader;
4023
4060
  if (idHeader !== void 0) headers[idHeader] = requestId;
@@ -4027,11 +4064,8 @@ var Pipeline = class {
4027
4064
  body: null
4028
4065
  };
4029
4066
  }
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
4067
  const headers = { ...this.#jsonHeaders };
4034
- const body = utf8(json);
4068
+ if (type !== null) headers["content-type"] = type;
4035
4069
  headers["content-length"] = String(body.byteLength);
4036
4070
  const idHeader = this.#jsonIdHeader;
4037
4071
  if (idHeader !== void 0) headers[idHeader] = requestId;
@@ -4039,8 +4073,128 @@ var Pipeline = class {
4039
4073
  status,
4040
4074
  headers,
4041
4075
  body,
4042
- text: json
4076
+ text
4077
+ };
4078
+ }
4079
+ /**
4080
+ * The fast lane through the `http:request` wrappers (registered when the request started), in the
4081
+ * request scope: what {@link #respond} does for the route, the handler run inside `next()` like
4082
+ * {@link #fastRun}. A JSON value or `undefined` is serialised there as `serializeResult` does, and
4083
+ * the reply completed from the template of the fast lane when the wrappers added no response header
4084
+ * (`event.responseHeaders`) nor cookie (see {@link #fastDeliver}). Never rejects.
4085
+ */
4086
+ #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));
4087
+ /** The operation wrapped by the `http:request` wrappers in the fast lane: the route run, its outcome checked. */
4088
+ #fastOperation(x, route) {
4089
+ const ctx = x.ctx;
4090
+ const plan = route.plan;
4091
+ if (plan.parseBody) return this.#fastOperationLater(x, route, NO_RESULT);
4092
+ let result;
4093
+ try {
4094
+ result = plan.definition.invoke(this.#resolve(plan.controller), ctx);
4095
+ } catch (error) {
4096
+ return this.#fastChecked(x, this.#failed(error, ctx, x.event));
4097
+ }
4098
+ if (isThenable(result)) return this.#fastOperationLater(x, route, result);
4099
+ return this.#fastChecked(x, this.#fastOutcome(x, route, result));
4100
+ }
4101
+ /** {@link #fastOperation} with a body to read (`pending` is {@link NO_RESULT}) or a thenable result. */
4102
+ async #fastOperationLater(x, route, pending) {
4103
+ const ctx = x.ctx;
4104
+ const plan = route.plan;
4105
+ let result = pending;
4106
+ try {
4107
+ if (result === NO_RESULT) {
4108
+ const controller = this.#resolve(plan.controller);
4109
+ await ctx.parseBody();
4110
+ result = plan.definition.invoke(controller, ctx);
4111
+ }
4112
+ if (isThenable(result)) result = await result;
4113
+ } catch (error) {
4114
+ return await this.#fastChecked(x, this.#failed(error, ctx, x.event));
4115
+ }
4116
+ return await this.#fastChecked(x, this.#fastOutcome(x, route, result));
4117
+ }
4118
+ /**
4119
+ * The outcome of a handler result in {@link #fastOperation}: for a plain JSON value or `undefined`,
4120
+ * the reply of `serializeResult` built directly (`x.fast` set); anything else as in the general
4121
+ * pipeline (see {@link #settle}). Never throws nor rejects (errors are mapped).
4122
+ */
4123
+ #fastOutcome(x, route, result) {
4124
+ const ctx = x.ctx;
4125
+ const event = x.event;
4126
+ const status = result === void 0 ? route.emptyStatus : route.jsonStatus;
4127
+ if (status === void 0 || ctx.hasResponseHeaders || result !== void 0 && !isPlainJson(result)) return this.#settle(ctx, route.plan, event, result);
4128
+ let reply;
4129
+ if (result === void 0) reply = {
4130
+ status,
4131
+ headers: fastRecord(),
4132
+ body: null
4043
4133
  };
4134
+ else {
4135
+ let json;
4136
+ try {
4137
+ json = JSON.stringify(result);
4138
+ if (json === void 0) throw new TypeError(`Cannot serialise a ${typeof result} as a response body`);
4139
+ } catch (error) {
4140
+ return this.#failed(error, ctx, event);
4141
+ }
4142
+ const headers = fastRecord();
4143
+ headers["content-type"] = ContentType.json;
4144
+ const body = utf8(json);
4145
+ headers["content-length"] = String(body.byteLength);
4146
+ reply = {
4147
+ status,
4148
+ headers,
4149
+ body,
4150
+ text: json,
4151
+ ownHeaders: true
4152
+ };
4153
+ }
4154
+ event.status = status;
4155
+ x.fast = true;
4156
+ return reply;
4157
+ }
4158
+ /**
4159
+ * What the general pipeline does with an outcome inside the wrappers (see {@link #respond}): its
4160
+ * headers and cookies checked, the async context of a streamed body captured; kept in `x.outcome`.
4161
+ */
4162
+ #fastChecked(x, pending) {
4163
+ if (pending instanceof Promise) return pending.then((outcome) => this.#fastKeep(x, outcome));
4164
+ return Promise.resolve(this.#fastKeep(x, pending));
4165
+ }
4166
+ #fastKeep(x, outcome) {
4167
+ const checked = x.fast ? outcome : this.#checked(outcome, x.ctx, x.event);
4168
+ if (checked !== RAW && isStreamed(checked.body)) x.run = captureContext();
4169
+ x.outcome = checked;
4170
+ return checked;
4171
+ }
4172
+ /**
4173
+ * Answers a fast-lane request once its wrappers settled: a reply of {@link #fastOutcome} gets the
4174
+ * headers of the template when nothing was added to it (no response header from the wrappers, no
4175
+ * cookie, not draining, a request id that can be sent), which is what {@link #complete} gives;
4176
+ * anything else is completed by {@link #deliver}. Never rejects.
4177
+ */
4178
+ #fastDeliver(x, failed, hookError) {
4179
+ const outcome = x.outcome;
4180
+ const ctx = x.ctx;
4181
+ if (outcome === void 0) return this.#fastUnhandled(x, failed ? hookError : NOT_CALLED);
4182
+ if (failed) this.#wrapperFailed(x, hookError);
4183
+ else if (x.fast && outcome !== RAW && !hasOwnKeys(x.event.responseHeaders) && !ctx.hasCookies && !this.draining && (this.#validIds || isHeaderValue(x.requestId))) {
4184
+ const reply = this.#templateReply(outcome.status, outcome.body, outcome.text, x.requestId, null);
4185
+ ctx.commit();
4186
+ ctx.releaseBody();
4187
+ x.answer(reply);
4188
+ return;
4189
+ }
4190
+ const streaming = this.#send(x, outcome, true, x.run);
4191
+ if (streaming === void 0) return void 0;
4192
+ return streaming.tracked.done.then((failure) => this.#streamed(x, streaming, failure));
4193
+ }
4194
+ /** {@link #fastDeliver} when a wrapper failed before the request was handled or did not call `next()`. */
4195
+ async #fastUnhandled(x, failure) {
4196
+ const streaming = this.#send(x, await this.#unhandled(x, failure), true, void 0);
4197
+ if (streaming !== void 0) this.#streamed(x, streaming, await streaming.tracked.done);
4044
4198
  }
4045
4199
  /** Ends a fast-lane request once its asynchronous part settled: its scope is closed, then it is over. */
4046
4200
  #end(x, scope, error) {
@@ -4225,16 +4379,41 @@ var Pipeline = class {
4225
4379
  hookError = error;
4226
4380
  }
4227
4381
  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;
4382
+ outcome = await this.#unhandled(x, failed ? hookError : NOT_CALLED);
4233
4383
  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
4384
+ } else if (failed) this.#wrapperFailed(x, hookError);
4385
+ const streaming = this.#send(x, outcome, recheck, run);
4386
+ if (streaming !== void 0) this.#streamed(x, streaming, await streaming.tracked.done);
4387
+ }
4388
+ /**
4389
+ * The reply of a request whose `http:request` wrapper failed before it was handled (`error`), or
4390
+ * did not call `next()` ({@link NOT_CALLED}): the error mapped and checked like a handled reply (an
4391
+ * exception mapper's `HttpResponse` may carry cookies). Never rejects.
4392
+ */
4393
+ async #unhandled(x, failure) {
4394
+ const ctx = x.ctx;
4395
+ const event = x.event;
4396
+ const error = failure === NOT_CALLED ? /* @__PURE__ */ new Error("An \"http:request\" hook wrapper did not call next()") : failure;
4397
+ event.error ??= error;
4398
+ const fallback = await this.#errorReply(error, ctx);
4399
+ const reply = this.#checkCookies(this.#checkHeaders(fallback, ctx, event), ctx, event);
4400
+ event.status = reply.status;
4401
+ return reply;
4402
+ }
4403
+ #wrapperFailed(x, error) {
4404
+ this.#logger.error("An \"http:request\" hook wrapper failed after the request was handled", {
4405
+ err: error,
4406
+ requestId: x.requestId
4237
4407
  });
4408
+ }
4409
+ /**
4410
+ * Completes the outcome of a request (`recheck`: wrappers ran) and hands its reply to the adapter.
4411
+ * Returns the tracking of a streamed body (read in `run`, the context of the handler, when it was
4412
+ * captured), whose end {@link #streamed} records.
4413
+ */
4414
+ #send(x, outcome, recheck, run) {
4415
+ const ctx = x.ctx;
4416
+ const event = x.event;
4238
4417
  if (outcome === RAW) {
4239
4418
  ctx.commit();
4240
4419
  const status = x.request.rawStatus?.() ?? 200;
@@ -4247,7 +4426,7 @@ var Pipeline = class {
4247
4426
  });
4248
4427
  return;
4249
4428
  }
4250
- const serialized = this.#complete(outcome, ctx, event, requestId, x.cors, x.preflight, recheck);
4429
+ const serialized = this.#complete(outcome, ctx, event, x.requestId, x.cors, x.preflight, recheck);
4251
4430
  const body = serialized.body;
4252
4431
  if (!isStreamed(body)) {
4253
4432
  const reply = adapterReply(serialized, body);
@@ -4265,11 +4444,18 @@ var Pipeline = class {
4265
4444
  }
4266
4445
  ctx.commit();
4267
4446
  x.answer(reply);
4268
- const failure = await tracked.done;
4269
- if (session !== void 0) this.#streams.delete(session);
4447
+ return {
4448
+ tracked,
4449
+ session
4450
+ };
4451
+ }
4452
+ /** A streamed body was sent or abandoned (`failure`: the stream failed). */
4453
+ #streamed(x, streaming, failure) {
4454
+ const ctx = x.ctx;
4455
+ if (streaming.session !== void 0) this.#streams.delete(streaming.session);
4270
4456
  if (failure !== void 0) this.#logger.error("The response stream failed", {
4271
4457
  err: failure,
4272
- requestId,
4458
+ requestId: x.requestId,
4273
4459
  path: ctx.path
4274
4460
  });
4275
4461
  ctx.releaseBody();
@@ -4672,6 +4858,8 @@ var Pipeline = class {
4672
4858
  };
4673
4859
  /** `#fastAsync` reads the body and runs the handler. */
4674
4860
  const NO_RESULT = Symbol("no result");
4861
+ /** An `http:request` wrapper returned without calling `next()` (see `Pipeline.#unhandled`). */
4862
+ const NOT_CALLED = Symbol("next() not called");
4675
4863
  /** A request being answered (internal): its context, its target and the promise of its reply. */
4676
4864
  var Exchange = class {
4677
4865
  request;
@@ -4686,6 +4874,12 @@ var Exchange = class {
4686
4874
  tracked = false;
4687
4875
  /** The asynchronous part of a fast-lane request (never rejects). */
4688
4876
  done = void 0;
4877
+ /** Fast lane with wrappers: the outcome of the route once checked (see `Pipeline.#fastKeep`). */
4878
+ outcome = void 0;
4879
+ /** Fast lane with wrappers: `outcome` is a JSON reply or a reply without body built by the lane. */
4880
+ fast = false;
4881
+ /** Fast lane with wrappers: the async context of the handler, for a streamed body. */
4882
+ run = void 0;
4689
4883
  #resolve;
4690
4884
  #reject;
4691
4885
  #event;
@@ -4735,6 +4929,11 @@ function requestEvent(request, ctx, startTime) {
4735
4929
  responseHeaders: {}
4736
4930
  };
4737
4931
  }
4932
+ /** Whether a record has an own enumerable property (`event.responseHeaders` was written to). */
4933
+ function hasOwnKeys(record) {
4934
+ for (const key in record) if (Object.hasOwn(record, key)) return true;
4935
+ return false;
4936
+ }
4738
4937
  /**
4739
4938
  * The decoded parameters of a raw request path matched against a route, in one pass: what
4740
4939
  * `matchRoutePath` then `decodePathValue` (see `Pipeline.#route`) give for a path they accept.
@@ -4787,6 +4986,22 @@ function isPlainJson(value) {
4787
4986
  } else if (prototype !== Array.prototype || ctor !== Array) return false;
4788
4987
  return typeof value.pipe !== "function";
4789
4988
  }
4989
+ /**
4990
+ * The content type of an `HttpResponse` the fast lane can serve: `null` without headers, its
4991
+ * `content-type` when that is its only header (lower-case name, a value every adapter can send);
4992
+ * `undefined` when it needs the general pipeline (other headers, cookies).
4993
+ */
4994
+ function fastResponseType(response) {
4995
+ if (response.cookies.length > 0) return void 0;
4996
+ const headers = response.headers;
4997
+ let type = null;
4998
+ for (const name of Object.keys(headers)) {
4999
+ const value = headers[name];
5000
+ if (name !== "content-type" || typeof value !== "string" || !isHeaderValue(value)) return void 0;
5001
+ type = value;
5002
+ }
5003
+ return type;
5004
+ }
4790
5005
  const ENCODER = new TextEncoder();
4791
5006
  /** UTF-8 encoding of a text body, like the serialiser (a pooled `Buffer` on Node.js, `TextEncoder` on Bun). */
4792
5007
  const utf8 = isBun() ? (text) => ENCODER.encode(text) : (text) => Buffer$1.from(text, "utf8");
@@ -4947,6 +5162,8 @@ function validateRoute(instance, plugin, value, prefix) {
4947
5162
  }
4948
5163
  const guards = route.guards ?? [];
4949
5164
  if (!Array.isArray(guards)) throw invalid(plugin, `the guards of the route ${where} must be an array`);
5165
+ const bodyLimit = route.bodyLimit === void 0 ? void 0 : parseByteSize(route.bodyLimit);
5166
+ 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)`);
4950
5167
  return {
4951
5168
  plugin,
4952
5169
  instance,
@@ -4956,7 +5173,8 @@ function validateRoute(instance, plugin, value, prefix) {
4956
5173
  name: route.name,
4957
5174
  handler: route.handler,
4958
5175
  meta: entries,
4959
- guards
5176
+ guards,
5177
+ bodyLimit
4960
5178
  };
4961
5179
  }
4962
5180
  function metaEntry(plugin, where, value) {
@@ -5940,7 +6158,7 @@ function buildRegistrations(ctx, options, raw, pluginRoutes) {
5940
6158
  invoke: (_plugin, http) => handler(http)
5941
6159
  });
5942
6160
  const instance = { instance: route.instance };
5943
- const body = bodyPlan({}, options.bodyLimit, options.uploads);
6161
+ const body = bodyPlan(route.bodyLimit === void 0 ? {} : { bodyLimit: route.bodyLimit }, options.bodyLimit, options.uploads);
5944
6162
  const chain = guards.map((guard) => guard.resolvable);
5945
6163
  const path = route.path;
5946
6164
  declared.push({