@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/README.md +85 -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 +264 -46
- 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 +327 -63
- 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
|
@@ -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
|
|
3893
|
-
*
|
|
3894
|
-
*
|
|
3895
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
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})
|
|
4011
|
-
*
|
|
4012
|
-
* `undefined` when the result or the context
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.#
|
|
4235
|
-
|
|
4236
|
-
|
|
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
|
-
|
|
4269
|
-
|
|
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({
|