@znight/http 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -10
- package/dist/compiler.d.mts +77 -2
- package/dist/compiler.mjs +177 -88
- package/dist/compiler.mjs.map +1 -1
- package/dist/index.d.mts +27 -3
- package/dist/index.mjs +230 -40
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/compiler/analyze.ts +3 -3
- package/src/compiler/index.ts +13 -0
- package/src/compiler/route-decorators.ts +45 -2
- package/src/compiler/route-groups.ts +216 -0
- package/src/compiler/validate.ts +15 -126
- package/src/decorators.ts +19 -3
- package/src/index.ts +1 -0
- package/src/pipeline.ts +279 -58
- package/src/plugin-routes.ts +19 -0
- package/src/plugin.ts +6 -1
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:
|
|
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
|
@@ -3889,20 +3889,23 @@ var Pipeline = class {
|
|
|
3889
3889
|
}
|
|
3890
3890
|
/**
|
|
3891
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
|
|
3893
|
-
*
|
|
3894
|
-
*
|
|
3895
|
-
*
|
|
3896
|
-
* final order (a template precomputed at boot).
|
|
3892
|
+
* of per request, without promises while the route stays synchronous. A request takes it when the
|
|
3893
|
+
* server is not draining and its method is the route's (not a `HEAD` served by a `GET` route); a
|
|
3894
|
+
* plain object or array returned by the handler is serialised as JSON, and `undefined` answered
|
|
3895
|
+
* without body, with the headers in their final order (a template precomputed at boot).
|
|
3897
3896
|
* A thenable result is awaited in the lane (see {@link #fastAsync}). Anything else — a malformed
|
|
3898
3897
|
* path (before the request starts), a thrown error or a rejection, an `HttpResponse`, a `Response`
|
|
3899
3898
|
* or any other result, headers or cookies set through the context — continues in the general
|
|
3900
3899
|
* pipeline, in the same request scope, with the same outcome.
|
|
3900
|
+
* With `http:request` wrappers, the handler runs inside them (see {@link #fastWrapped}); listeners
|
|
3901
|
+
* get the event once the reply is ready, as in the general pipeline.
|
|
3901
3902
|
*/
|
|
3902
3903
|
#fast(route, request) {
|
|
3903
3904
|
const plan = route.plan;
|
|
3905
|
+
if (this.draining || request.method !== route.method) return this.handle(plan, request);
|
|
3904
3906
|
const hooks = this.#hooks;
|
|
3905
|
-
|
|
3907
|
+
const wrapped = hooks.hasWrappers("http:request");
|
|
3908
|
+
const startTime = wrapped || hooks.hasListeners("http:request") ? performance.now() : void 0;
|
|
3906
3909
|
const url = request.url;
|
|
3907
3910
|
const mark = url.indexOf("?");
|
|
3908
3911
|
const path = mark === -1 ? url : url.slice(0, mark);
|
|
@@ -3918,13 +3921,13 @@ var Pipeline = class {
|
|
|
3918
3921
|
return Promise.reject(error);
|
|
3919
3922
|
}
|
|
3920
3923
|
const scope = this.#container.openScope([[HttpTokens.Context, ctx], [HttpTokens.RequestId, ctx.requestId]]);
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
done.tracked = true;
|
|
3926
|
-
return this.#emitted(done);
|
|
3924
|
+
if (wrapped) {
|
|
3925
|
+
const x = new Exchange(request, ctx, ctx.requestId, route.target, startTime);
|
|
3926
|
+
x.done = scope.run(this.#fastWrapped, x, route);
|
|
3927
|
+
return this.#fastExchange(x, scope);
|
|
3927
3928
|
}
|
|
3929
|
+
const done = scope.run(this.#fastRun, route, request, ctx, startTime);
|
|
3930
|
+
if (done instanceof Exchange) return this.#fastExchange(done, scope);
|
|
3928
3931
|
const closing = scope.close();
|
|
3929
3932
|
if (closing === void 0) {
|
|
3930
3933
|
ctx.complete();
|
|
@@ -3935,7 +3938,7 @@ var Pipeline = class {
|
|
|
3935
3938
|
closing.then(() => this.#over(x), (error) => this.#disposalFailed(x, error));
|
|
3936
3939
|
}
|
|
3937
3940
|
if (!hooks.hasListeners("http:request")) return Promise.resolve(done);
|
|
3938
|
-
const event = requestEvent(request, ctx, performance.now());
|
|
3941
|
+
const event = requestEvent(request, ctx, startTime ?? performance.now());
|
|
3939
3942
|
event.status = done.status;
|
|
3940
3943
|
return Promise.resolve(done).then((reply) => {
|
|
3941
3944
|
hooks.emit("http:request", event);
|
|
@@ -3943,20 +3946,31 @@ var Pipeline = class {
|
|
|
3943
3946
|
});
|
|
3944
3947
|
}
|
|
3945
3948
|
/**
|
|
3949
|
+
* The reply of a fast-lane request whose asynchronous part runs in `x.done`: the request ends once
|
|
3950
|
+
* it settled (see {@link #end}); listeners get the event once the reply is ready.
|
|
3951
|
+
*/
|
|
3952
|
+
#fastExchange(x, scope) {
|
|
3953
|
+
x.done.then(() => this.#end(x, scope, void 0), (error) => this.#end(x, scope, error));
|
|
3954
|
+
if (!this.#hooks.hasListeners("http:request")) return x.replied;
|
|
3955
|
+
x.tracked = true;
|
|
3956
|
+
return this.#emitted(x);
|
|
3957
|
+
}
|
|
3958
|
+
/**
|
|
3946
3959
|
* The fast lane in the request scope: the reply when the handler answered synchronously with a
|
|
3947
3960
|
* plain JSON value or `undefined`, otherwise the exchange finishing the request (see {@link #fast}).
|
|
3961
|
+
* `startTime`: the request has `http:request` listeners, its event starts with the exchange.
|
|
3948
3962
|
*/
|
|
3949
|
-
#fastRun = (route, request, ctx) => {
|
|
3963
|
+
#fastRun = (route, request, ctx, startTime) => {
|
|
3950
3964
|
const plan = route.plan;
|
|
3951
|
-
if (plan.parseBody) return this.#fastLater(route, request, ctx, NO_RESULT);
|
|
3965
|
+
if (plan.parseBody) return this.#fastLater(route, request, ctx, NO_RESULT, startTime);
|
|
3952
3966
|
let result;
|
|
3953
3967
|
let reply;
|
|
3954
3968
|
try {
|
|
3955
3969
|
result = plan.definition.invoke(this.#resolve(plan.controller), ctx);
|
|
3956
|
-
if (isThenable(result)) return this.#fastLater(route, request, ctx, result);
|
|
3970
|
+
if (isThenable(result)) return this.#fastLater(route, request, ctx, result, startTime);
|
|
3957
3971
|
reply = this.#fastReply(route, ctx, result);
|
|
3958
3972
|
} catch (error) {
|
|
3959
|
-
const x = new Exchange(request, ctx, ctx.requestId, route.target);
|
|
3973
|
+
const x = new Exchange(request, ctx, ctx.requestId, route.target, startTime);
|
|
3960
3974
|
x.done = this.#respond(x, () => this.#failed(error, ctx, x.event));
|
|
3961
3975
|
return x;
|
|
3962
3976
|
}
|
|
@@ -3965,13 +3979,13 @@ var Pipeline = class {
|
|
|
3965
3979
|
ctx.releaseBody();
|
|
3966
3980
|
return reply;
|
|
3967
3981
|
}
|
|
3968
|
-
const x = new Exchange(request, ctx, ctx.requestId, route.target);
|
|
3982
|
+
const x = new Exchange(request, ctx, ctx.requestId, route.target, startTime);
|
|
3969
3983
|
x.done = this.#respond(x, () => this.#settle(ctx, plan, x.event, result));
|
|
3970
3984
|
return x;
|
|
3971
3985
|
};
|
|
3972
3986
|
/** 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);
|
|
3987
|
+
#fastLater(route, request, ctx, result, startTime) {
|
|
3988
|
+
const x = new Exchange(request, ctx, ctx.requestId, route.target, startTime);
|
|
3975
3989
|
x.done = this.#fastAsync(x, route, result);
|
|
3976
3990
|
return x;
|
|
3977
3991
|
}
|
|
@@ -4017,7 +4031,19 @@ var Pipeline = class {
|
|
|
4017
4031
|
if (status === void 0 || ctx.hasResponseHeaders || ctx.hasCookies || this.draining) return void 0;
|
|
4018
4032
|
const requestId = ctx.requestId;
|
|
4019
4033
|
if (!this.#validIds && !isHeaderValue(requestId)) return void 0;
|
|
4020
|
-
if (result === void 0)
|
|
4034
|
+
if (result === void 0) return this.#templateReply(status, null, void 0, requestId);
|
|
4035
|
+
if (!isPlainJson(result)) return void 0;
|
|
4036
|
+
const json = JSON.stringify(result);
|
|
4037
|
+
if (json === void 0) throw new TypeError(`Cannot serialise a ${typeof result} as a response body`);
|
|
4038
|
+
return this.#templateReply(status, utf8(json), json, requestId);
|
|
4039
|
+
}
|
|
4040
|
+
/**
|
|
4041
|
+
* The reply of a JSON body (`text` set) or of no body, its headers copied from the template of
|
|
4042
|
+
* the fast lane (see {@link #jsonHeaders}), completed with the length and the request id (one that
|
|
4043
|
+
* can be sent).
|
|
4044
|
+
*/
|
|
4045
|
+
#templateReply(status, body, text, requestId) {
|
|
4046
|
+
if (text === void 0) {
|
|
4021
4047
|
const headers = { ...this.#emptyHeaders };
|
|
4022
4048
|
const idHeader = this.#emptyIdHeader;
|
|
4023
4049
|
if (idHeader !== void 0) headers[idHeader] = requestId;
|
|
@@ -4027,11 +4053,7 @@ var Pipeline = class {
|
|
|
4027
4053
|
body: null
|
|
4028
4054
|
};
|
|
4029
4055
|
}
|
|
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
4056
|
const headers = { ...this.#jsonHeaders };
|
|
4034
|
-
const body = utf8(json);
|
|
4035
4057
|
headers["content-length"] = String(body.byteLength);
|
|
4036
4058
|
const idHeader = this.#jsonIdHeader;
|
|
4037
4059
|
if (idHeader !== void 0) headers[idHeader] = requestId;
|
|
@@ -4039,8 +4061,128 @@ var Pipeline = class {
|
|
|
4039
4061
|
status,
|
|
4040
4062
|
headers,
|
|
4041
4063
|
body,
|
|
4042
|
-
text
|
|
4064
|
+
text
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
4067
|
+
/**
|
|
4068
|
+
* The fast lane through the `http:request` wrappers (registered when the request started), in the
|
|
4069
|
+
* request scope: what {@link #respond} does for the route, the handler run inside `next()` like
|
|
4070
|
+
* {@link #fastRun}. A JSON value or `undefined` is serialised there as `serializeResult` does, and
|
|
4071
|
+
* the reply completed from the template of the fast lane when the wrappers added no response header
|
|
4072
|
+
* (`event.responseHeaders`) nor cookie (see {@link #fastDeliver}). Never rejects.
|
|
4073
|
+
*/
|
|
4074
|
+
#fastWrapped = (x, route) => this.#hooks.run("http:request", x.event, () => this.#fastOperation(x, route)).then(() => this.#fastDeliver(x, false, void 0), (error) => this.#fastDeliver(x, true, error));
|
|
4075
|
+
/** The operation wrapped by the `http:request` wrappers in the fast lane: the route run, its outcome checked. */
|
|
4076
|
+
#fastOperation(x, route) {
|
|
4077
|
+
const ctx = x.ctx;
|
|
4078
|
+
const plan = route.plan;
|
|
4079
|
+
if (plan.parseBody) return this.#fastOperationLater(x, route, NO_RESULT);
|
|
4080
|
+
let result;
|
|
4081
|
+
try {
|
|
4082
|
+
result = plan.definition.invoke(this.#resolve(plan.controller), ctx);
|
|
4083
|
+
} catch (error) {
|
|
4084
|
+
return this.#fastChecked(x, this.#failed(error, ctx, x.event));
|
|
4085
|
+
}
|
|
4086
|
+
if (isThenable(result)) return this.#fastOperationLater(x, route, result);
|
|
4087
|
+
return this.#fastChecked(x, this.#fastOutcome(x, route, result));
|
|
4088
|
+
}
|
|
4089
|
+
/** {@link #fastOperation} with a body to read (`pending` is {@link NO_RESULT}) or a thenable result. */
|
|
4090
|
+
async #fastOperationLater(x, route, pending) {
|
|
4091
|
+
const ctx = x.ctx;
|
|
4092
|
+
const plan = route.plan;
|
|
4093
|
+
let result = pending;
|
|
4094
|
+
try {
|
|
4095
|
+
if (result === NO_RESULT) {
|
|
4096
|
+
const controller = this.#resolve(plan.controller);
|
|
4097
|
+
await ctx.parseBody();
|
|
4098
|
+
result = plan.definition.invoke(controller, ctx);
|
|
4099
|
+
}
|
|
4100
|
+
if (isThenable(result)) result = await result;
|
|
4101
|
+
} catch (error) {
|
|
4102
|
+
return await this.#fastChecked(x, this.#failed(error, ctx, x.event));
|
|
4103
|
+
}
|
|
4104
|
+
return await this.#fastChecked(x, this.#fastOutcome(x, route, result));
|
|
4105
|
+
}
|
|
4106
|
+
/**
|
|
4107
|
+
* The outcome of a handler result in {@link #fastOperation}: for a plain JSON value or `undefined`,
|
|
4108
|
+
* the reply of `serializeResult` built directly (`x.fast` set); anything else as in the general
|
|
4109
|
+
* pipeline (see {@link #settle}). Never throws nor rejects (errors are mapped).
|
|
4110
|
+
*/
|
|
4111
|
+
#fastOutcome(x, route, result) {
|
|
4112
|
+
const ctx = x.ctx;
|
|
4113
|
+
const event = x.event;
|
|
4114
|
+
const status = result === void 0 ? route.emptyStatus : route.jsonStatus;
|
|
4115
|
+
if (status === void 0 || ctx.hasResponseHeaders || result !== void 0 && !isPlainJson(result)) return this.#settle(ctx, route.plan, event, result);
|
|
4116
|
+
let reply;
|
|
4117
|
+
if (result === void 0) reply = {
|
|
4118
|
+
status,
|
|
4119
|
+
headers: fastRecord(),
|
|
4120
|
+
body: null
|
|
4043
4121
|
};
|
|
4122
|
+
else {
|
|
4123
|
+
let json;
|
|
4124
|
+
try {
|
|
4125
|
+
json = JSON.stringify(result);
|
|
4126
|
+
if (json === void 0) throw new TypeError(`Cannot serialise a ${typeof result} as a response body`);
|
|
4127
|
+
} catch (error) {
|
|
4128
|
+
return this.#failed(error, ctx, event);
|
|
4129
|
+
}
|
|
4130
|
+
const headers = fastRecord();
|
|
4131
|
+
headers["content-type"] = ContentType.json;
|
|
4132
|
+
const body = utf8(json);
|
|
4133
|
+
headers["content-length"] = String(body.byteLength);
|
|
4134
|
+
reply = {
|
|
4135
|
+
status,
|
|
4136
|
+
headers,
|
|
4137
|
+
body,
|
|
4138
|
+
text: json,
|
|
4139
|
+
ownHeaders: true
|
|
4140
|
+
};
|
|
4141
|
+
}
|
|
4142
|
+
event.status = status;
|
|
4143
|
+
x.fast = true;
|
|
4144
|
+
return reply;
|
|
4145
|
+
}
|
|
4146
|
+
/**
|
|
4147
|
+
* What the general pipeline does with an outcome inside the wrappers (see {@link #respond}): its
|
|
4148
|
+
* headers and cookies checked, the async context of a streamed body captured; kept in `x.outcome`.
|
|
4149
|
+
*/
|
|
4150
|
+
#fastChecked(x, pending) {
|
|
4151
|
+
if (pending instanceof Promise) return pending.then((outcome) => this.#fastKeep(x, outcome));
|
|
4152
|
+
return Promise.resolve(this.#fastKeep(x, pending));
|
|
4153
|
+
}
|
|
4154
|
+
#fastKeep(x, outcome) {
|
|
4155
|
+
const checked = x.fast ? outcome : this.#checked(outcome, x.ctx, x.event);
|
|
4156
|
+
if (checked !== RAW && isStreamed(checked.body)) x.run = captureContext();
|
|
4157
|
+
x.outcome = checked;
|
|
4158
|
+
return checked;
|
|
4159
|
+
}
|
|
4160
|
+
/**
|
|
4161
|
+
* Answers a fast-lane request once its wrappers settled: a reply of {@link #fastOutcome} gets the
|
|
4162
|
+
* headers of the template when nothing was added to it (no response header from the wrappers, no
|
|
4163
|
+
* cookie, not draining, a request id that can be sent), which is what {@link #complete} gives;
|
|
4164
|
+
* anything else is completed by {@link #deliver}. Never rejects.
|
|
4165
|
+
*/
|
|
4166
|
+
#fastDeliver(x, failed, hookError) {
|
|
4167
|
+
const outcome = x.outcome;
|
|
4168
|
+
const ctx = x.ctx;
|
|
4169
|
+
if (outcome === void 0) return this.#fastUnhandled(x, failed ? hookError : NOT_CALLED);
|
|
4170
|
+
if (failed) this.#wrapperFailed(x, hookError);
|
|
4171
|
+
else if (x.fast && outcome !== RAW && !hasOwnKeys(x.event.responseHeaders) && !ctx.hasCookies && !this.draining && (this.#validIds || isHeaderValue(x.requestId))) {
|
|
4172
|
+
const reply = this.#templateReply(outcome.status, outcome.body, outcome.text, x.requestId);
|
|
4173
|
+
ctx.commit();
|
|
4174
|
+
ctx.releaseBody();
|
|
4175
|
+
x.answer(reply);
|
|
4176
|
+
return;
|
|
4177
|
+
}
|
|
4178
|
+
const streaming = this.#send(x, outcome, true, x.run);
|
|
4179
|
+
if (streaming === void 0) return void 0;
|
|
4180
|
+
return streaming.tracked.done.then((failure) => this.#streamed(x, streaming, failure));
|
|
4181
|
+
}
|
|
4182
|
+
/** {@link #fastDeliver} when a wrapper failed before the request was handled or did not call `next()`. */
|
|
4183
|
+
async #fastUnhandled(x, failure) {
|
|
4184
|
+
const streaming = this.#send(x, await this.#unhandled(x, failure), true, void 0);
|
|
4185
|
+
if (streaming !== void 0) this.#streamed(x, streaming, await streaming.tracked.done);
|
|
4044
4186
|
}
|
|
4045
4187
|
/** Ends a fast-lane request once its asynchronous part settled: its scope is closed, then it is over. */
|
|
4046
4188
|
#end(x, scope, error) {
|
|
@@ -4225,16 +4367,41 @@ var Pipeline = class {
|
|
|
4225
4367
|
hookError = error;
|
|
4226
4368
|
}
|
|
4227
4369
|
if (outcome === void 0) {
|
|
4228
|
-
|
|
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;
|
|
4370
|
+
outcome = await this.#unhandled(x, failed ? hookError : NOT_CALLED);
|
|
4233
4371
|
recheck = true;
|
|
4234
|
-
} else if (failed) this.#
|
|
4235
|
-
|
|
4236
|
-
|
|
4372
|
+
} else if (failed) this.#wrapperFailed(x, hookError);
|
|
4373
|
+
const streaming = this.#send(x, outcome, recheck, run);
|
|
4374
|
+
if (streaming !== void 0) this.#streamed(x, streaming, await streaming.tracked.done);
|
|
4375
|
+
}
|
|
4376
|
+
/**
|
|
4377
|
+
* The reply of a request whose `http:request` wrapper failed before it was handled (`error`), or
|
|
4378
|
+
* did not call `next()` ({@link NOT_CALLED}): the error mapped and checked like a handled reply (an
|
|
4379
|
+
* exception mapper's `HttpResponse` may carry cookies). Never rejects.
|
|
4380
|
+
*/
|
|
4381
|
+
async #unhandled(x, failure) {
|
|
4382
|
+
const ctx = x.ctx;
|
|
4383
|
+
const event = x.event;
|
|
4384
|
+
const error = failure === NOT_CALLED ? /* @__PURE__ */ new Error("An \"http:request\" hook wrapper did not call next()") : failure;
|
|
4385
|
+
event.error ??= error;
|
|
4386
|
+
const fallback = await this.#errorReply(error, ctx);
|
|
4387
|
+
const reply = this.#checkCookies(this.#checkHeaders(fallback, ctx, event), ctx, event);
|
|
4388
|
+
event.status = reply.status;
|
|
4389
|
+
return reply;
|
|
4390
|
+
}
|
|
4391
|
+
#wrapperFailed(x, error) {
|
|
4392
|
+
this.#logger.error("An \"http:request\" hook wrapper failed after the request was handled", {
|
|
4393
|
+
err: error,
|
|
4394
|
+
requestId: x.requestId
|
|
4237
4395
|
});
|
|
4396
|
+
}
|
|
4397
|
+
/**
|
|
4398
|
+
* Completes the outcome of a request (`recheck`: wrappers ran) and hands its reply to the adapter.
|
|
4399
|
+
* Returns the tracking of a streamed body (read in `run`, the context of the handler, when it was
|
|
4400
|
+
* captured), whose end {@link #streamed} records.
|
|
4401
|
+
*/
|
|
4402
|
+
#send(x, outcome, recheck, run) {
|
|
4403
|
+
const ctx = x.ctx;
|
|
4404
|
+
const event = x.event;
|
|
4238
4405
|
if (outcome === RAW) {
|
|
4239
4406
|
ctx.commit();
|
|
4240
4407
|
const status = x.request.rawStatus?.() ?? 200;
|
|
@@ -4247,7 +4414,7 @@ var Pipeline = class {
|
|
|
4247
4414
|
});
|
|
4248
4415
|
return;
|
|
4249
4416
|
}
|
|
4250
|
-
const serialized = this.#complete(outcome, ctx, event, requestId, x.cors, x.preflight, recheck);
|
|
4417
|
+
const serialized = this.#complete(outcome, ctx, event, x.requestId, x.cors, x.preflight, recheck);
|
|
4251
4418
|
const body = serialized.body;
|
|
4252
4419
|
if (!isStreamed(body)) {
|
|
4253
4420
|
const reply = adapterReply(serialized, body);
|
|
@@ -4265,11 +4432,18 @@ var Pipeline = class {
|
|
|
4265
4432
|
}
|
|
4266
4433
|
ctx.commit();
|
|
4267
4434
|
x.answer(reply);
|
|
4268
|
-
|
|
4269
|
-
|
|
4435
|
+
return {
|
|
4436
|
+
tracked,
|
|
4437
|
+
session
|
|
4438
|
+
};
|
|
4439
|
+
}
|
|
4440
|
+
/** A streamed body was sent or abandoned (`failure`: the stream failed). */
|
|
4441
|
+
#streamed(x, streaming, failure) {
|
|
4442
|
+
const ctx = x.ctx;
|
|
4443
|
+
if (streaming.session !== void 0) this.#streams.delete(streaming.session);
|
|
4270
4444
|
if (failure !== void 0) this.#logger.error("The response stream failed", {
|
|
4271
4445
|
err: failure,
|
|
4272
|
-
requestId,
|
|
4446
|
+
requestId: x.requestId,
|
|
4273
4447
|
path: ctx.path
|
|
4274
4448
|
});
|
|
4275
4449
|
ctx.releaseBody();
|
|
@@ -4672,6 +4846,8 @@ var Pipeline = class {
|
|
|
4672
4846
|
};
|
|
4673
4847
|
/** `#fastAsync` reads the body and runs the handler. */
|
|
4674
4848
|
const NO_RESULT = Symbol("no result");
|
|
4849
|
+
/** An `http:request` wrapper returned without calling `next()` (see `Pipeline.#unhandled`). */
|
|
4850
|
+
const NOT_CALLED = Symbol("next() not called");
|
|
4675
4851
|
/** A request being answered (internal): its context, its target and the promise of its reply. */
|
|
4676
4852
|
var Exchange = class {
|
|
4677
4853
|
request;
|
|
@@ -4686,6 +4862,12 @@ var Exchange = class {
|
|
|
4686
4862
|
tracked = false;
|
|
4687
4863
|
/** The asynchronous part of a fast-lane request (never rejects). */
|
|
4688
4864
|
done = void 0;
|
|
4865
|
+
/** Fast lane with wrappers: the outcome of the route once checked (see `Pipeline.#fastKeep`). */
|
|
4866
|
+
outcome = void 0;
|
|
4867
|
+
/** Fast lane with wrappers: `outcome` is a JSON reply or a reply without body built by the lane. */
|
|
4868
|
+
fast = false;
|
|
4869
|
+
/** Fast lane with wrappers: the async context of the handler, for a streamed body. */
|
|
4870
|
+
run = void 0;
|
|
4689
4871
|
#resolve;
|
|
4690
4872
|
#reject;
|
|
4691
4873
|
#event;
|
|
@@ -4735,6 +4917,11 @@ function requestEvent(request, ctx, startTime) {
|
|
|
4735
4917
|
responseHeaders: {}
|
|
4736
4918
|
};
|
|
4737
4919
|
}
|
|
4920
|
+
/** Whether a record has an own enumerable property (`event.responseHeaders` was written to). */
|
|
4921
|
+
function hasOwnKeys(record) {
|
|
4922
|
+
for (const key in record) if (Object.hasOwn(record, key)) return true;
|
|
4923
|
+
return false;
|
|
4924
|
+
}
|
|
4738
4925
|
/**
|
|
4739
4926
|
* The decoded parameters of a raw request path matched against a route, in one pass: what
|
|
4740
4927
|
* `matchRoutePath` then `decodePathValue` (see `Pipeline.#route`) give for a path they accept.
|
|
@@ -4947,6 +5134,8 @@ function validateRoute(instance, plugin, value, prefix) {
|
|
|
4947
5134
|
}
|
|
4948
5135
|
const guards = route.guards ?? [];
|
|
4949
5136
|
if (!Array.isArray(guards)) throw invalid(plugin, `the guards of the route ${where} must be an array`);
|
|
5137
|
+
const bodyLimit = route.bodyLimit === void 0 ? void 0 : parseByteSize(route.bodyLimit);
|
|
5138
|
+
if (route.bodyLimit !== void 0 && bodyLimit === void 0) throw invalid(plugin, `invalid bodyLimit ${describe(route.bodyLimit)} of the route ${where}: expected a size in bytes (1048576, '100kb', Infinity)`);
|
|
4950
5139
|
return {
|
|
4951
5140
|
plugin,
|
|
4952
5141
|
instance,
|
|
@@ -4956,7 +5145,8 @@ function validateRoute(instance, plugin, value, prefix) {
|
|
|
4956
5145
|
name: route.name,
|
|
4957
5146
|
handler: route.handler,
|
|
4958
5147
|
meta: entries,
|
|
4959
|
-
guards
|
|
5148
|
+
guards,
|
|
5149
|
+
bodyLimit
|
|
4960
5150
|
};
|
|
4961
5151
|
}
|
|
4962
5152
|
function metaEntry(plugin, where, value) {
|
|
@@ -5940,7 +6130,7 @@ function buildRegistrations(ctx, options, raw, pluginRoutes) {
|
|
|
5940
6130
|
invoke: (_plugin, http) => handler(http)
|
|
5941
6131
|
});
|
|
5942
6132
|
const instance = { instance: route.instance };
|
|
5943
|
-
const body = bodyPlan({}, options.bodyLimit, options.uploads);
|
|
6133
|
+
const body = bodyPlan(route.bodyLimit === void 0 ? {} : { bodyLimit: route.bodyLimit }, options.bodyLimit, options.uploads);
|
|
5944
6134
|
const chain = guards.map((guard) => guard.resolvable);
|
|
5945
6135
|
const path = route.path;
|
|
5946
6136
|
declared.push({
|