@znight/http 0.1.1 → 0.1.2
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/LICENSE +1 -1
- package/README.md +4 -0
- package/dist/{adapter-CGSceVEN.d.mts → adapter-J0UXFnNM.d.mts} +7 -1
- package/dist/compiler.d.mts +1 -1
- package/dist/express.d.mts +1 -1
- package/dist/express.mjs +1 -1
- package/dist/fastify.d.mts +1 -1
- package/dist/fastify.mjs +57 -22
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.d.mts +1 -1
- package/dist/hono.mjs +112 -38
- package/dist/hono.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +251 -157
- package/dist/index.mjs.map +1 -1
- package/dist/{node-BUnkdSsa.mjs → node-Clm1Ia7T.mjs} +2 -2
- package/dist/{node-BUnkdSsa.mjs.map → node-Clm1Ia7T.mjs.map} +1 -1
- package/dist/{node-io-BY1ZDLRQ.mjs → node-io-CVnqpVNS.mjs} +52 -26
- package/dist/{node-io-BY1ZDLRQ.mjs.map → node-io-CVnqpVNS.mjs.map} +1 -1
- package/dist/node.d.mts +1 -1
- package/dist/node.mjs +1 -1
- package/package.json +4 -4
- package/src/adapter.ts +6 -0
- package/src/adapters/fastify.ts +66 -21
- package/src/adapters/hono.ts +135 -47
- package/src/adapters/node-io.ts +57 -24
- package/src/context.ts +7 -4
- package/src/pipeline.ts +237 -174
- package/src/plugin.ts +9 -2
- package/src/query.ts +7 -1
- package/src/serialize.ts +51 -11
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { n as RouteTable, t as nodeAdapter } from "./node-
|
|
2
|
-
import { $ as UnprocessableEntityException, A as serializeCookie, B as ConflictException, C as decodeCookieValue, D as parseCookieHeader, E as isCookieName, F as isStreamSource, G as InternalServerErrorException, H as GatewayTimeoutException, I as toByteStream, J as NotImplementedException, K as MethodNotAllowedException, L as trackStream, M as captureContext, N as discardStream, O as parseSetCookie, P as isByteStream, Q as UnauthorizedException, R as BadGatewayException, S as checkCookie, T as givenCookieKey, U as GoneException, V as ForbiddenException, W as HttpException, X as ServiceUnavailableException, Y as PayloadTooLargeException, Z as TooManyRequestsException, at as isBodylessStatus, et as UnsupportedMediaTypeException, h as isEventStreamType, it as toIssueDetail, j as serializeResponseCookie, k as serializeClearCookie, nt as isHttpException, ot as isValidStatus, q as NotFoundException, rt as issue, st as statusText, tt as ValidationException, w as encodeCookieValue, x as DEFAULT_COOKIE_OPTIONS, z as BadRequestException } from "./node-io-
|
|
1
|
+
import { n as RouteTable, t as nodeAdapter } from "./node-Clm1Ia7T.mjs";
|
|
2
|
+
import { $ as UnprocessableEntityException, A as serializeCookie, B as ConflictException, C as decodeCookieValue, D as parseCookieHeader, E as isCookieName, F as isStreamSource, G as InternalServerErrorException, H as GatewayTimeoutException, I as toByteStream, J as NotImplementedException, K as MethodNotAllowedException, L as trackStream, M as captureContext, N as discardStream, O as parseSetCookie, P as isByteStream, Q as UnauthorizedException, R as BadGatewayException, S as checkCookie, T as givenCookieKey, U as GoneException, V as ForbiddenException, W as HttpException, X as ServiceUnavailableException, Y as PayloadTooLargeException, Z as TooManyRequestsException, at as isBodylessStatus, et as UnsupportedMediaTypeException, h as isEventStreamType, it as toIssueDetail, j as serializeResponseCookie, k as serializeClearCookie, nt as isHttpException, ot as isValidStatus, q as NotFoundException, rt as issue, st as statusText, tt as ValidationException, w as encodeCookieValue, x as DEFAULT_COOKIE_OPTIONS, z as BadRequestException } from "./node-io-CVnqpVNS.mjs";
|
|
3
3
|
import { a as isMediaTypePattern, i as contentTypeFor, n as parseByteSize, o as matchesMediaType, r as OCTET_STREAM, s as mediaTypeEssence, t as formatByteSize } from "./size-D8rsQcV0.mjs";
|
|
4
|
-
import { i as isThenable, n as errorMessage, t as emptyRecord } from "./utils-DCbu3a4w.mjs";
|
|
4
|
+
import { i as isThenable, n as errorMessage, r as isBun, t as emptyRecord } from "./utils-DCbu3a4w.mjs";
|
|
5
5
|
import { RoutePathError, compareRoutePaths, compareStrings, decodePathValue, joinPaths, matchRoutePath, normalizePath, parseRoutePath } from "./path.mjs";
|
|
6
6
|
import { Readable } from "node:stream";
|
|
7
7
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
@@ -823,7 +823,7 @@ var RequestContext = class {
|
|
|
823
823
|
this.#source = source;
|
|
824
824
|
this.#plan = init.body;
|
|
825
825
|
this.#cookieSettings = init.cookies ?? DEFAULT_COOKIE_SETTINGS;
|
|
826
|
-
const { path, query } = splitUrl(source.url);
|
|
826
|
+
const { path, query } = init.url ?? splitUrl(source.url);
|
|
827
827
|
this.#rawQuery = query;
|
|
828
828
|
this.requestId = init.requestId;
|
|
829
829
|
this.method = source.method;
|
|
@@ -910,7 +910,7 @@ var RequestContext = class {
|
|
|
910
910
|
}
|
|
911
911
|
/** The `Set-Cookie` lines of {@link setCookie} / {@link clearCookie}, with their keys, in order (internal). */
|
|
912
912
|
get cookieLines() {
|
|
913
|
-
return this.#jar === void 0 ?
|
|
913
|
+
return this.#jar === void 0 ? NO_LINES : [...this.#jar].map(([key, entry]) => ({
|
|
914
914
|
key,
|
|
915
915
|
line: entry.line
|
|
916
916
|
}));
|
|
@@ -1071,6 +1071,7 @@ function withoutLifetime(options) {
|
|
|
1071
1071
|
}
|
|
1072
1072
|
const EMPTY_HEADERS = Object.freeze({});
|
|
1073
1073
|
const NO_COOKIES$1 = Object.freeze([]);
|
|
1074
|
+
const NO_LINES = Object.freeze([]);
|
|
1074
1075
|
const NO_FILES$1 = Object.freeze(emptyRecord());
|
|
1075
1076
|
// src/decorators.ts
|
|
1076
1077
|
function marker() {}
|
|
@@ -3347,6 +3348,11 @@ function isHttpResponse(value) {
|
|
|
3347
3348
|
}
|
|
3348
3349
|
// src/serialize.ts
|
|
3349
3350
|
const ENCODER = new TextEncoder();
|
|
3351
|
+
/**
|
|
3352
|
+
* UTF-8 encoding of a text body: a pooled `Buffer` (a `Uint8Array`) is several times cheaper than
|
|
3353
|
+
* `TextEncoder` on Node.js, `TextEncoder` is the fastest on Bun.
|
|
3354
|
+
*/
|
|
3355
|
+
const utf8 = isBun() ? (text) => ENCODER.encode(text) : (text) => Buffer$1.from(text, "utf8");
|
|
3350
3356
|
/** Content types set by the serialiser. */
|
|
3351
3357
|
const ContentType = Object.freeze({
|
|
3352
3358
|
json: "application/json; charset=utf-8",
|
|
@@ -3387,10 +3393,20 @@ function serializeResult(result, options) {
|
|
|
3387
3393
|
const finalStatus = options.status ?? status ?? defaultStatus(options.routeMethod, payload);
|
|
3388
3394
|
const headers = emptyRecord();
|
|
3389
3395
|
const body = encode(payload, headers);
|
|
3390
|
-
|
|
3391
|
-
if (options.
|
|
3392
|
-
|
|
3393
|
-
|
|
3396
|
+
let own = true;
|
|
3397
|
+
if (options.headers !== void 0) {
|
|
3398
|
+
assignHeaders(headers, options.headers);
|
|
3399
|
+
own = false;
|
|
3400
|
+
}
|
|
3401
|
+
if (options.context !== void 0) {
|
|
3402
|
+
assignHeaders(headers, options.context);
|
|
3403
|
+
own = false;
|
|
3404
|
+
}
|
|
3405
|
+
if (extra !== void 0) {
|
|
3406
|
+
assignHeaders(headers, extra);
|
|
3407
|
+
own = false;
|
|
3408
|
+
}
|
|
3409
|
+
return withCookies(finish(finalStatus, headers, body, options.method, own), cookies);
|
|
3394
3410
|
}
|
|
3395
3411
|
/**
|
|
3396
3412
|
* Serialises an event stream (`@Sse` result, `HttpResponse.sse()`): status 200 unless `@HttpCode`
|
|
@@ -3466,7 +3482,11 @@ function addMissingHeaders(target, source) {
|
|
|
3466
3482
|
if (value !== void 0 && target[key] === void 0) target[key] = typeof value === "string" ? value : [...value];
|
|
3467
3483
|
}
|
|
3468
3484
|
}
|
|
3469
|
-
|
|
3485
|
+
/**
|
|
3486
|
+
* The reply of a body: a string (see {@link encode}) is sent as its UTF-8 bytes, with its text.
|
|
3487
|
+
* `own`: only {@link encode} set `headers`, which are then valid for a text body.
|
|
3488
|
+
*/
|
|
3489
|
+
function finish(status, headers, body, method, own = false) {
|
|
3470
3490
|
if (isBodylessStatus(status)) {
|
|
3471
3491
|
discard(body);
|
|
3472
3492
|
delete headers["content-type"];
|
|
@@ -3485,18 +3505,34 @@ function finish(status, headers, body, method) {
|
|
|
3485
3505
|
body: null
|
|
3486
3506
|
};
|
|
3487
3507
|
}
|
|
3488
|
-
return {
|
|
3508
|
+
if (typeof body !== "string") return {
|
|
3489
3509
|
status,
|
|
3490
3510
|
headers,
|
|
3491
3511
|
body
|
|
3492
3512
|
};
|
|
3513
|
+
return own ? {
|
|
3514
|
+
status,
|
|
3515
|
+
headers,
|
|
3516
|
+
body: utf8(body),
|
|
3517
|
+
text: body,
|
|
3518
|
+
ownHeaders: true
|
|
3519
|
+
} : {
|
|
3520
|
+
status,
|
|
3521
|
+
headers,
|
|
3522
|
+
body: utf8(body),
|
|
3523
|
+
text: body
|
|
3524
|
+
};
|
|
3493
3525
|
}
|
|
3494
3526
|
function discard(body) {
|
|
3495
3527
|
if (body instanceof ReadableStream) discardStream(body);
|
|
3496
3528
|
}
|
|
3529
|
+
/**
|
|
3530
|
+
* The body of a payload, its `content-type` and, for bytes and text, its `content-length` set in
|
|
3531
|
+
* `headers`. Text (a string, JSON) is returned as a string, encoded as UTF-8 by {@link finish}.
|
|
3532
|
+
*/
|
|
3497
3533
|
function encode(payload, headers) {
|
|
3498
3534
|
if (payload === void 0) return null;
|
|
3499
|
-
if (typeof payload === "string") return
|
|
3535
|
+
if (typeof payload === "string") return text$1(payload, headers, ContentType.text);
|
|
3500
3536
|
if (payload instanceof Uint8Array) return bytes(payload, headers, ContentType.bytes);
|
|
3501
3537
|
if (payload instanceof ArrayBuffer) return bytes(new Uint8Array(payload), headers, ContentType.bytes);
|
|
3502
3538
|
if (ArrayBuffer.isView(payload)) return bytes(new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength), headers, ContentType.bytes);
|
|
@@ -3517,13 +3553,18 @@ function encode(payload, headers) {
|
|
|
3517
3553
|
if (isSseBody(payload)) throw new TypeError("An event stream (HttpResponse.sse()) cannot be serialised here");
|
|
3518
3554
|
const json = JSON.stringify(payload);
|
|
3519
3555
|
if (json === void 0) throw new TypeError(`Cannot serialise a ${typeof payload} as a response body`);
|
|
3520
|
-
return
|
|
3556
|
+
return text$1(json, headers, ContentType.json);
|
|
3521
3557
|
}
|
|
3522
3558
|
function bytes(data, headers, type) {
|
|
3523
3559
|
headers["content-type"] = type;
|
|
3524
3560
|
headers["content-length"] = String(data.byteLength);
|
|
3525
3561
|
return data;
|
|
3526
3562
|
}
|
|
3563
|
+
function text$1(data, headers, type) {
|
|
3564
|
+
headers["content-type"] = type;
|
|
3565
|
+
headers["content-length"] = String(Buffer$1.byteLength(data, "utf8"));
|
|
3566
|
+
return data;
|
|
3567
|
+
}
|
|
3527
3568
|
function isNodeReadable(value) {
|
|
3528
3569
|
return value instanceof Readable || value !== null && typeof value === "object" && typeof value.pipe === "function" && typeof value.on === "function" && typeof value.read === "function";
|
|
3529
3570
|
}
|
|
@@ -3555,6 +3596,10 @@ const DEFAULT_SSE_SETTINGS = Object.freeze({
|
|
|
3555
3596
|
const RAW = Symbol("raw response");
|
|
3556
3597
|
const REQUEST_ID = /^[\w\-.:@/+=]{1,128}$/;
|
|
3557
3598
|
const MAX_PATH_IN_MESSAGE = 200;
|
|
3599
|
+
/** The default request id generator: a random UUID. */
|
|
3600
|
+
function randomRequestId() {
|
|
3601
|
+
return crypto.randomUUID();
|
|
3602
|
+
}
|
|
3558
3603
|
/**
|
|
3559
3604
|
* The request pipeline (architecture §9.1):
|
|
3560
3605
|
* `runInScope({ HttpContext, RequestId }) → hooks.run('http:request') → guards → body → interceptors
|
|
@@ -3566,6 +3611,10 @@ var Pipeline = class {
|
|
|
3566
3611
|
#logger;
|
|
3567
3612
|
#options;
|
|
3568
3613
|
#table;
|
|
3614
|
+
/** The `securityHeaders` option as entries (lower-case names, values validated at boot). */
|
|
3615
|
+
#securityHeaders;
|
|
3616
|
+
/** Every request id can be sent: an incoming one matched `REQUEST_ID`, the generated ones are UUIDs. */
|
|
3617
|
+
#validIds;
|
|
3569
3618
|
#inflight = 0;
|
|
3570
3619
|
#idle = [];
|
|
3571
3620
|
/** Open event streams. */
|
|
@@ -3578,6 +3627,8 @@ var Pipeline = class {
|
|
|
3578
3627
|
this.#logger = logger;
|
|
3579
3628
|
this.#options = options;
|
|
3580
3629
|
this.#table = table;
|
|
3630
|
+
this.#validIds = options.generateRequestId === randomRequestId;
|
|
3631
|
+
this.#securityHeaders = Object.entries(options.securityHeaders ?? {}).map(([name, value]) => [name.toLowerCase(), value]);
|
|
3581
3632
|
}
|
|
3582
3633
|
/** Number of requests being processed. */
|
|
3583
3634
|
get inflight() {
|
|
@@ -3603,28 +3654,25 @@ var Pipeline = class {
|
|
|
3603
3654
|
* Handles a request routed to `plan` by the adapter. The request stays in flight (and its scope
|
|
3604
3655
|
* open) until a streamed or file response body has been fully read or cancelled by the adapter.
|
|
3605
3656
|
*/
|
|
3606
|
-
|
|
3657
|
+
handle(plan, request) {
|
|
3607
3658
|
const startTime = performance.now();
|
|
3608
|
-
this.#enter();
|
|
3609
|
-
let served;
|
|
3610
3659
|
try {
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
this.#leaveWhen(served);
|
|
3660
|
+
return this.#isPreflight(request) ? this.#preflight(request, startTime) : this.#route(plan, request, startTime);
|
|
3661
|
+
} catch (error) {
|
|
3662
|
+
return Promise.reject(error);
|
|
3615
3663
|
}
|
|
3616
3664
|
}
|
|
3617
3665
|
#route(plan, request, startTime, rerouted = false) {
|
|
3618
|
-
const
|
|
3619
|
-
const raw = matchRoutePath(plan.path, path);
|
|
3666
|
+
const url = splitUrl(request.url);
|
|
3667
|
+
const raw = matchRoutePath(plan.path, url.path);
|
|
3620
3668
|
if (raw === void 0) return this.#fallback(request, void 0, startTime, !rerouted);
|
|
3621
3669
|
if (plan.redirect === true) {
|
|
3622
|
-
const canonical = sameOriginPath(path.slice(0, -1));
|
|
3623
|
-
const
|
|
3624
|
-
return this.#serve(request, void 0, emptyRecord(), {
|
|
3670
|
+
const canonical = sameOriginPath(url.path.slice(0, -1));
|
|
3671
|
+
const target = {
|
|
3625
3672
|
kind: "redirect",
|
|
3626
|
-
location
|
|
3627
|
-
}
|
|
3673
|
+
location: url.query === "" ? canonical : `${canonical}?${url.query}`
|
|
3674
|
+
};
|
|
3675
|
+
return this.#serve(request, url, void 0, emptyRecord(), target, startTime, plan.body);
|
|
3628
3676
|
}
|
|
3629
3677
|
const params = emptyRecord();
|
|
3630
3678
|
let malformed;
|
|
@@ -3640,18 +3688,15 @@ var Pipeline = class {
|
|
|
3640
3688
|
kind: "error",
|
|
3641
3689
|
error: new BadRequestException(`Malformed URL parameter "${malformed}"`)
|
|
3642
3690
|
};
|
|
3643
|
-
return this.#serve(request, plan.info, params, target, startTime, plan.body);
|
|
3691
|
+
return this.#serve(request, url, plan.info, params, target, startTime, plan.body);
|
|
3644
3692
|
}
|
|
3645
3693
|
/** Handles an unmatched request (404, 405, automatic OPTIONS) or a framework error. */
|
|
3646
|
-
|
|
3694
|
+
fallback(request, error) {
|
|
3647
3695
|
const startTime = performance.now();
|
|
3648
|
-
this.#enter();
|
|
3649
|
-
let served;
|
|
3650
3696
|
try {
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
this.#leaveWhen(served);
|
|
3697
|
+
return this.#fallback(request, error, startTime);
|
|
3698
|
+
} catch (failure) {
|
|
3699
|
+
return Promise.reject(failure);
|
|
3655
3700
|
}
|
|
3656
3701
|
}
|
|
3657
3702
|
#fallback(request, error, startTime, reroute = true) {
|
|
@@ -3661,10 +3706,11 @@ var Pipeline = class {
|
|
|
3661
3706
|
kind: "error",
|
|
3662
3707
|
error: frameworkError(error)
|
|
3663
3708
|
};
|
|
3664
|
-
return this.#serve(request, void 0, emptyRecord(), target, startTime, body);
|
|
3709
|
+
return this.#serve(request, splitUrl(request.url), void 0, emptyRecord(), target, startTime, body);
|
|
3665
3710
|
}
|
|
3666
3711
|
if (this.#isPreflight(request)) return this.#preflight(request, startTime);
|
|
3667
|
-
const
|
|
3712
|
+
const url = splitUrl(request.url);
|
|
3713
|
+
const path = url.path;
|
|
3668
3714
|
if (reroute) {
|
|
3669
3715
|
const plan = this.#table.find(request.method, path);
|
|
3670
3716
|
if (plan !== void 0) return this.#route(plan, request, startTime, true);
|
|
@@ -3683,63 +3729,98 @@ var Pipeline = class {
|
|
|
3683
3729
|
kind: "error",
|
|
3684
3730
|
error: new MethodNotAllowedException(`Method ${request.method} is not allowed on ${truncate(path)}`, void 0, { headers: { allow: allowHeader(allowed) } })
|
|
3685
3731
|
};
|
|
3686
|
-
return this.#serve(request, void 0, emptyRecord(), target, startTime, body);
|
|
3732
|
+
return this.#serve(request, url, void 0, emptyRecord(), target, startTime, body);
|
|
3687
3733
|
}
|
|
3688
3734
|
/** A CORS preflight (with the `cors` option): answered without running any route. */
|
|
3689
3735
|
#isPreflight(request) {
|
|
3690
3736
|
return this.#options.cors !== void 0 && isPreflight(request.method, request.headers);
|
|
3691
3737
|
}
|
|
3692
3738
|
#preflight(request, startTime) {
|
|
3693
|
-
const
|
|
3739
|
+
const url = splitUrl(request.url);
|
|
3740
|
+
const allowed = this.#table.allowedMethods(url.path);
|
|
3694
3741
|
const target = {
|
|
3695
3742
|
kind: "preflight",
|
|
3696
3743
|
allow: allowed.length === 0 ? void 0 : allowHeader(allowed)
|
|
3697
3744
|
};
|
|
3698
|
-
return this.#serve(request, void 0, emptyRecord(), target, startTime, this.#options.body);
|
|
3699
|
-
}
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3745
|
+
return this.#serve(request, url, void 0, emptyRecord(), target, startTime, this.#options.body);
|
|
3746
|
+
}
|
|
3747
|
+
#serve(request, url, route, params, target, startTime, body) {
|
|
3748
|
+
this.#enter();
|
|
3749
|
+
let requestId;
|
|
3750
|
+
let ctx;
|
|
3751
|
+
let event;
|
|
3752
|
+
try {
|
|
3753
|
+
requestId = this.#requestId(request);
|
|
3754
|
+
const proxy = this.#options.trustProxy;
|
|
3755
|
+
ctx = new RequestContext(request, {
|
|
3756
|
+
requestId,
|
|
3757
|
+
adapter: this.#options.adapter,
|
|
3758
|
+
url,
|
|
3759
|
+
route,
|
|
3760
|
+
params,
|
|
3761
|
+
body,
|
|
3762
|
+
cookies: this.#options.cookies ?? DEFAULT_COOKIE_SETTINGS,
|
|
3763
|
+
client: proxy === void 0 ? void 0 : proxy.resolve(request.remoteAddress ?? request.ip, request.headers, request.encrypted === true)
|
|
3764
|
+
});
|
|
3765
|
+
if (this.draining && target.kind === "route" && target.plan.definition.sse !== void 0) ctx.abort("shutdown");
|
|
3766
|
+
event = {
|
|
3767
|
+
method: request.method,
|
|
3768
|
+
path: ctx.path,
|
|
3769
|
+
route: route?.path,
|
|
3770
|
+
requestId,
|
|
3771
|
+
headers: request.headers,
|
|
3772
|
+
context: ctx,
|
|
3773
|
+
startTime,
|
|
3774
|
+
responseHeaders: {}
|
|
3775
|
+
};
|
|
3776
|
+
} catch (error) {
|
|
3777
|
+
this.#leave();
|
|
3778
|
+
return Promise.reject(error);
|
|
3779
|
+
}
|
|
3723
3780
|
let outcome;
|
|
3724
|
-
let reply;
|
|
3725
3781
|
let cors;
|
|
3726
3782
|
const preflight = target.kind === "preflight";
|
|
3727
|
-
let
|
|
3728
|
-
|
|
3729
|
-
|
|
3783
|
+
let resolve = () => void 0;
|
|
3784
|
+
let reject = () => void 0;
|
|
3785
|
+
const replied = new Promise((onReply, onError) => {
|
|
3786
|
+
resolve = onReply;
|
|
3787
|
+
reject = onError;
|
|
3730
3788
|
});
|
|
3731
|
-
|
|
3789
|
+
let answered = false;
|
|
3790
|
+
const answer = (reply) => {
|
|
3791
|
+
answered = true;
|
|
3792
|
+
resolve(reply);
|
|
3793
|
+
};
|
|
3794
|
+
/** The request is over (response body sent or abandoned, scope closed). */
|
|
3795
|
+
const over = () => {
|
|
3796
|
+
ctx.complete();
|
|
3797
|
+
try {
|
|
3798
|
+
if (!answered) {
|
|
3799
|
+
const failure = /* @__PURE__ */ new Error("Request not handled");
|
|
3800
|
+
const reply = serializeError(500, this.#internalBody(failure), ctx.method);
|
|
3801
|
+
const completed = this.#complete(reply, ctx, event, requestId, cors, preflight, true);
|
|
3802
|
+
event.status = 500;
|
|
3803
|
+
answer(completed);
|
|
3804
|
+
}
|
|
3805
|
+
} catch (error) {
|
|
3806
|
+
reject(error);
|
|
3807
|
+
} finally {
|
|
3808
|
+
this.#leave();
|
|
3809
|
+
}
|
|
3810
|
+
};
|
|
3811
|
+
this.#container.runInScope(async () => {
|
|
3732
3812
|
let hookError;
|
|
3733
3813
|
let failed = false;
|
|
3734
3814
|
let run;
|
|
3735
3815
|
if (this.#options.cors !== void 0) cors = await this.#corsDecision(request, requestId);
|
|
3816
|
+
let recheck = this.#hooks.hasWrappers("http:request");
|
|
3736
3817
|
try {
|
|
3737
|
-
await this.#hooks.run("http:request", event, async () => {
|
|
3738
|
-
outcome = await this.#process(ctx, target, event, cors);
|
|
3739
|
-
if (outcome !== RAW
|
|
3740
|
-
run = captureContext();
|
|
3818
|
+
if (recheck) await this.#hooks.run("http:request", event, async () => {
|
|
3819
|
+
outcome = this.#checked(await this.#process(ctx, target, event, cors), ctx, event);
|
|
3820
|
+
if (outcome !== RAW && isStreamed(outcome.body)) run = captureContext();
|
|
3741
3821
|
return outcome;
|
|
3742
3822
|
});
|
|
3823
|
+
else outcome = this.#checked(await this.#process(ctx, target, event, cors), ctx, event);
|
|
3743
3824
|
} catch (error) {
|
|
3744
3825
|
failed = true;
|
|
3745
3826
|
hookError = error;
|
|
@@ -3750,33 +3831,41 @@ var Pipeline = class {
|
|
|
3750
3831
|
const fallback = await this.#errorReply(error, ctx);
|
|
3751
3832
|
outcome = this.#checkCookies(this.#checkHeaders(fallback, ctx, event), ctx, event);
|
|
3752
3833
|
event.status = outcome.status;
|
|
3834
|
+
recheck = true;
|
|
3753
3835
|
} else if (failed) this.#logger.error("An \"http:request\" hook wrapper failed after the request was handled", {
|
|
3754
3836
|
err: hookError,
|
|
3755
3837
|
requestId
|
|
3756
3838
|
});
|
|
3757
3839
|
if (outcome === RAW) {
|
|
3758
3840
|
ctx.commit();
|
|
3759
|
-
|
|
3841
|
+
const status = request.rawStatus?.() ?? 200;
|
|
3842
|
+
event.status ??= status;
|
|
3843
|
+
answer({
|
|
3844
|
+
status,
|
|
3845
|
+
headers: {},
|
|
3846
|
+
body: null,
|
|
3847
|
+
raw: true
|
|
3848
|
+
});
|
|
3760
3849
|
return;
|
|
3761
3850
|
}
|
|
3762
|
-
const serialized = this.#complete(outcome, ctx, event, requestId, cors, preflight);
|
|
3851
|
+
const serialized = this.#complete(outcome, ctx, event, requestId, cors, preflight, recheck);
|
|
3763
3852
|
const body = serialized.body;
|
|
3764
|
-
if (!(body
|
|
3765
|
-
reply = adapterReply(serialized, body);
|
|
3853
|
+
if (!isStreamed(body)) {
|
|
3854
|
+
const reply = adapterReply(serialized, body);
|
|
3766
3855
|
ctx.commit();
|
|
3767
|
-
replied();
|
|
3768
3856
|
ctx.releaseBody();
|
|
3857
|
+
answer(reply);
|
|
3769
3858
|
return;
|
|
3770
3859
|
}
|
|
3771
3860
|
const tracked = trackStream(body, run ?? captureContext(), () => ctx.abort("client"));
|
|
3772
3861
|
const session = serialized.sse;
|
|
3773
|
-
reply = adapterReply(serialized, tracked.stream, body instanceof Blob ? body : void 0);
|
|
3862
|
+
const reply = adapterReply(serialized, tracked.stream, body instanceof Blob ? body : void 0);
|
|
3774
3863
|
if (session !== void 0) {
|
|
3775
3864
|
this.#streams.add(session);
|
|
3776
3865
|
if (this.draining) session.close("shutdown");
|
|
3777
3866
|
}
|
|
3778
3867
|
ctx.commit();
|
|
3779
|
-
|
|
3868
|
+
answer(reply);
|
|
3780
3869
|
const failure = await tracked.done;
|
|
3781
3870
|
if (session !== void 0) this.#streams.delete(session);
|
|
3782
3871
|
if (failure !== void 0) this.#logger.error("The response stream failed", {
|
|
@@ -3785,49 +3874,30 @@ var Pipeline = class {
|
|
|
3785
3874
|
path: ctx.path
|
|
3786
3875
|
});
|
|
3787
3876
|
ctx.releaseBody();
|
|
3788
|
-
}, [[HttpTokens.Context, ctx], [HttpTokens.RequestId, requestId]]).then(() => {
|
|
3789
|
-
|
|
3790
|
-
}, (error) => {
|
|
3791
|
-
ctx.complete();
|
|
3877
|
+
}, [[HttpTokens.Context, ctx], [HttpTokens.RequestId, requestId]]).then(over, (error) => {
|
|
3878
|
+
over();
|
|
3792
3879
|
this.#logger.error("Failed to dispose request-scoped providers", {
|
|
3793
3880
|
err: error,
|
|
3794
3881
|
requestId
|
|
3795
3882
|
});
|
|
3796
3883
|
});
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
return {
|
|
3803
|
-
reply: {
|
|
3804
|
-
status,
|
|
3805
|
-
headers: {},
|
|
3806
|
-
body: null,
|
|
3807
|
-
raw: true
|
|
3808
|
-
},
|
|
3809
|
-
done
|
|
3810
|
-
};
|
|
3811
|
-
}
|
|
3812
|
-
if (reply === void 0) {
|
|
3813
|
-
const failure = /* @__PURE__ */ new Error("Request not handled");
|
|
3814
|
-
reply = this.#complete(serializeError(500, this.#internalBody(failure), ctx.method), ctx, event, requestId, cors, preflight);
|
|
3815
|
-
event.status = 500;
|
|
3816
|
-
}
|
|
3817
|
-
this.#emit(event);
|
|
3818
|
-
return {
|
|
3819
|
-
reply,
|
|
3820
|
-
done
|
|
3821
|
-
};
|
|
3884
|
+
if (!this.#hooks.hasListeners("http:request")) return replied;
|
|
3885
|
+
return replied.then((reply) => {
|
|
3886
|
+
this.#hooks.emit("http:request", event);
|
|
3887
|
+
return reply;
|
|
3888
|
+
});
|
|
3822
3889
|
}
|
|
3823
3890
|
/**
|
|
3824
3891
|
* Adds the headers of the `http:request` wrappers, then the framework headers (see
|
|
3825
|
-
* {@link #frameworkHeaders}),
|
|
3892
|
+
* {@link #frameworkHeaders}). When wrappers ran (`recheck`), checks the final headers again (see
|
|
3893
|
+
* {@link #checkHeaders}): the reply was checked before, and the cookies and framework headers are
|
|
3894
|
+
* valid by construction.
|
|
3826
3895
|
*/
|
|
3827
|
-
#complete(reply, ctx, event, requestId, cors, preflight) {
|
|
3896
|
+
#complete(reply, ctx, event, requestId, cors, preflight, recheck) {
|
|
3828
3897
|
addMissingHeaders(reply.headers, event.responseHeaders);
|
|
3829
3898
|
applyCookies(reply, ctx);
|
|
3830
3899
|
this.#frameworkHeaders(reply.headers, requestId, cors, preflight);
|
|
3900
|
+
if (!recheck) return reply;
|
|
3831
3901
|
const checked = this.#checkHeaders(reply, ctx, event);
|
|
3832
3902
|
if (checked !== reply) {
|
|
3833
3903
|
applyCookies(checked, ctx);
|
|
@@ -3835,6 +3905,12 @@ var Pipeline = class {
|
|
|
3835
3905
|
}
|
|
3836
3906
|
return checked;
|
|
3837
3907
|
}
|
|
3908
|
+
/** Checks the headers (unless only the serialiser set them), then the cookies of a reply. */
|
|
3909
|
+
#checked(outcome, ctx, event) {
|
|
3910
|
+
if (outcome === RAW) return outcome;
|
|
3911
|
+
const checked = outcome.ownHeaders === true ? outcome : this.#checkHeaders(outcome, ctx, event);
|
|
3912
|
+
return this.#checkCookies(checked, ctx, event);
|
|
3913
|
+
}
|
|
3838
3914
|
/**
|
|
3839
3915
|
* Serialises the cookies of an `HttpResponse` (`withCookie`) with the plugin settings (defaults,
|
|
3840
3916
|
* signing). A cookie breaking a rule that depends on them (a prefix with the `secure` default,
|
|
@@ -3869,7 +3945,7 @@ var Pipeline = class {
|
|
|
3869
3945
|
*/
|
|
3870
3946
|
#frameworkHeaders(headers, requestId, cors, preflight) {
|
|
3871
3947
|
const header = this.#options.requestIdHeader;
|
|
3872
|
-
if (header !== void 0 && headers[header] === void 0 && isHeaderValue(requestId)) headers[header] = requestId;
|
|
3948
|
+
if (header !== void 0 && headers[header] === void 0 && (this.#validIds || isHeaderValue(requestId))) headers[header] = requestId;
|
|
3873
3949
|
const policy = this.#options.cors;
|
|
3874
3950
|
if (policy !== void 0) {
|
|
3875
3951
|
if (!preflight) {
|
|
@@ -3878,8 +3954,7 @@ var Pipeline = class {
|
|
|
3878
3954
|
}
|
|
3879
3955
|
if (policy.varyOrigin) mergeVary(headers, "origin");
|
|
3880
3956
|
}
|
|
3881
|
-
const
|
|
3882
|
-
if (security !== void 0) addMissingHeaders(headers, security);
|
|
3957
|
+
for (const [name, value] of this.#securityHeaders) if (headers[name] === void 0) headers[name] = value;
|
|
3883
3958
|
if (this.draining) headers["connection"] = "close";
|
|
3884
3959
|
}
|
|
3885
3960
|
/**
|
|
@@ -3921,7 +3996,12 @@ var Pipeline = class {
|
|
|
3921
3996
|
this.#log(error, ctx);
|
|
3922
3997
|
return serializeError(500, this.#internalBody(error), ctx.method);
|
|
3923
3998
|
}
|
|
3924
|
-
|
|
3999
|
+
/** The outcome of a request: its route run, or a reply of the framework. Never rejects (errors are mapped). */
|
|
4000
|
+
#process(ctx, target, event, cors) {
|
|
4001
|
+
return target.kind === "route" ? this.#invoke(ctx, target.plan, event) : this.#answer(ctx, target, event, cors);
|
|
4002
|
+
}
|
|
4003
|
+
/** Answers a request without running a route. Never rejects. */
|
|
4004
|
+
async #answer(ctx, target, event, cors) {
|
|
3925
4005
|
try {
|
|
3926
4006
|
switch (target.kind) {
|
|
3927
4007
|
case "error": throw target.error;
|
|
@@ -3944,45 +4024,55 @@ var Pipeline = class {
|
|
|
3944
4024
|
status: 308,
|
|
3945
4025
|
headers: { location: target.location }
|
|
3946
4026
|
}));
|
|
3947
|
-
default: return await this.#invoke(ctx, target.plan, event);
|
|
3948
4027
|
}
|
|
3949
4028
|
} catch (error) {
|
|
3950
|
-
|
|
3951
|
-
const reply = await this.#errorReply(error, ctx);
|
|
3952
|
-
event.status = reply.status;
|
|
3953
|
-
return reply;
|
|
4029
|
+
return await this.#failed(error, ctx, event);
|
|
3954
4030
|
}
|
|
3955
4031
|
}
|
|
4032
|
+
/** The reply of an error (see {@link #errorReply}), recorded on the event. */
|
|
4033
|
+
async #failed(error, ctx, event) {
|
|
4034
|
+
event.error = error;
|
|
4035
|
+
const reply = await this.#errorReply(error, ctx);
|
|
4036
|
+
event.status = reply.status;
|
|
4037
|
+
return reply;
|
|
4038
|
+
}
|
|
4039
|
+
/** Runs a route: guards, body, interceptors, handler, serialisation. Never rejects (errors are mapped). */
|
|
3956
4040
|
async #invoke(ctx, plan, event) {
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
if (
|
|
3967
|
-
|
|
4041
|
+
try {
|
|
4042
|
+
const controller = this.#resolve(plan.controller);
|
|
4043
|
+
for (const guard of plan.guards) if (await this.#resolve(guard).canActivate(ctx) !== true) throw new ForbiddenException("Forbidden resource");
|
|
4044
|
+
if (plan.parseBody) await ctx.parseBody();
|
|
4045
|
+
const definition = plan.definition;
|
|
4046
|
+
const interceptors = plan.interceptors;
|
|
4047
|
+
let result;
|
|
4048
|
+
if (interceptors.length === 0) {
|
|
4049
|
+
result = definition.invoke(controller, ctx);
|
|
4050
|
+
if (isThenable(result)) result = await result;
|
|
4051
|
+
} else {
|
|
4052
|
+
const chain = async (index) => {
|
|
4053
|
+
if (index === interceptors.length) return await definition.invoke(controller, ctx);
|
|
4054
|
+
return await this.#resolve(interceptors[index]).intercept(ctx, () => chain(index + 1));
|
|
4055
|
+
};
|
|
4056
|
+
result = await chain(0);
|
|
4057
|
+
}
|
|
4058
|
+
if (definition.rawResponse === true && !(result instanceof Response)) return RAW;
|
|
4059
|
+
const options = {
|
|
4060
|
+
method: ctx.method,
|
|
4061
|
+
routeMethod: definition.method,
|
|
4062
|
+
status: definition.status,
|
|
4063
|
+
headers: definition.headers,
|
|
4064
|
+
context: ctx.hasResponseHeaders ? ctx.responseHeaders : void 0
|
|
3968
4065
|
};
|
|
3969
|
-
|
|
4066
|
+
const sse = definition.sse;
|
|
4067
|
+
let reply;
|
|
4068
|
+
if (sse !== void 0 && !(result instanceof Response) && !isHttpResponse(result)) {
|
|
4069
|
+
if (!isSseSource(result)) throw new TypeError(`@Sse handler ${displayName(plan.info.controller)}.${definition.handler} must return an AsyncIterable, an Iterable or a ReadableStream of SseEvent (received ${describeResult(result)})`);
|
|
4070
|
+
reply = serializeSseReply(this.#openStream(result, sseOptions(sse), ctx), void 0, options);
|
|
4071
|
+
} else reply = isHttpResponse(result) ? await this.#serialize(result, ctx, options, sse) : serializeResult(result, options);
|
|
4072
|
+
return this.#finish(event, reply);
|
|
4073
|
+
} catch (error) {
|
|
4074
|
+
return await this.#failed(error, ctx, event);
|
|
3970
4075
|
}
|
|
3971
|
-
if (definition.rawResponse === true && !(result instanceof Response)) return RAW;
|
|
3972
|
-
const options = {
|
|
3973
|
-
method: ctx.method,
|
|
3974
|
-
routeMethod: definition.method,
|
|
3975
|
-
status: definition.status,
|
|
3976
|
-
headers: definition.headers,
|
|
3977
|
-
context: ctx.hasResponseHeaders ? ctx.responseHeaders : void 0
|
|
3978
|
-
};
|
|
3979
|
-
const sse = definition.sse;
|
|
3980
|
-
let reply;
|
|
3981
|
-
if (sse !== void 0 && !(result instanceof Response) && !isHttpResponse(result)) {
|
|
3982
|
-
if (!isSseSource(result)) throw new TypeError(`@Sse handler ${displayName(plan.info.controller)}.${definition.handler} must return an AsyncIterable, an Iterable or a ReadableStream of SseEvent (received ${describeResult(result)})`);
|
|
3983
|
-
reply = serializeSseReply(this.#openStream(result, sseOptions(sse), ctx), void 0, options);
|
|
3984
|
-
} else reply = await this.#serialize(result, ctx, options, sse);
|
|
3985
|
-
return this.#finish(event, reply);
|
|
3986
4076
|
}
|
|
3987
4077
|
/** Serialises a handler (or exception mapper) result; file bodies are resolved, event streams opened. */
|
|
3988
4078
|
async #serialize(result, ctx, options, route) {
|
|
@@ -4132,17 +4222,9 @@ var Pipeline = class {
|
|
|
4132
4222
|
}
|
|
4133
4223
|
return this.#options.generateRequestId();
|
|
4134
4224
|
}
|
|
4135
|
-
#emit(event) {
|
|
4136
|
-
if (this.#hooks.hasListeners("http:request")) this.#hooks.emit("http:request", event);
|
|
4137
|
-
}
|
|
4138
4225
|
#enter() {
|
|
4139
4226
|
this.#inflight++;
|
|
4140
4227
|
}
|
|
4141
|
-
/** Leaves once the request is over (immediately when it failed before being served). */
|
|
4142
|
-
#leaveWhen(served) {
|
|
4143
|
-
if (served === void 0) this.#leave();
|
|
4144
|
-
else served.done.then(() => this.#leave());
|
|
4145
|
-
}
|
|
4146
4228
|
#leave() {
|
|
4147
4229
|
this.#inflight--;
|
|
4148
4230
|
if (this.#inflight === 0 && this.#idle.length > 0) {
|
|
@@ -4159,6 +4241,13 @@ var Pipeline = class {
|
|
|
4159
4241
|
* before a quiet stream's first byte, and lifts idle timeouts.
|
|
4160
4242
|
*/
|
|
4161
4243
|
function adapterReply(serialized, body, blob) {
|
|
4244
|
+
const text = serialized.text;
|
|
4245
|
+
if (text !== void 0) return {
|
|
4246
|
+
status: serialized.status,
|
|
4247
|
+
headers: serialized.headers,
|
|
4248
|
+
body,
|
|
4249
|
+
text
|
|
4250
|
+
};
|
|
4162
4251
|
const reply = {
|
|
4163
4252
|
status: serialized.status,
|
|
4164
4253
|
headers: serialized.headers,
|
|
@@ -4180,7 +4269,7 @@ function adapterReply(serialized, body, blob) {
|
|
|
4180
4269
|
* one in place. One line per cookie.
|
|
4181
4270
|
*/
|
|
4182
4271
|
function applyCookies(reply, ctx) {
|
|
4183
|
-
const own = reply.cookieLines ??
|
|
4272
|
+
const own = reply.cookieLines ?? NO_COOKIE_LINES;
|
|
4184
4273
|
const context = ctx.cookieLines;
|
|
4185
4274
|
if (own.length === 0 && context.length === 0) return;
|
|
4186
4275
|
const merged = /* @__PURE__ */ new Map();
|
|
@@ -4192,6 +4281,11 @@ function applyCookies(reply, ctx) {
|
|
|
4192
4281
|
else if (existing !== void 0) raw = [...existing];
|
|
4193
4282
|
reply.headers["set-cookie"] = [...raw, ...merged.values()];
|
|
4194
4283
|
}
|
|
4284
|
+
const NO_COOKIE_LINES = Object.freeze([]);
|
|
4285
|
+
/** Whether a reply body is streamed (a file is read through a stream too). */
|
|
4286
|
+
function isStreamed(body) {
|
|
4287
|
+
return body instanceof ReadableStream || body instanceof Blob;
|
|
4288
|
+
}
|
|
4195
4289
|
/** `SseOptions` of the `sse` metadata of a route (`heartbeat: 0` disables the heartbeat). */
|
|
4196
4290
|
function sseOptions(definition) {
|
|
4197
4291
|
return {
|
|
@@ -5029,7 +5123,7 @@ function resolve$1(options) {
|
|
|
5029
5123
|
if (typeof drainTimeoutMs !== "number" || !(drainTimeoutMs >= 0)) throw invalidOption("drainTimeoutMs", "a positive number (or Infinity)", drainTimeoutMs);
|
|
5030
5124
|
const requestIdHeader = options.requestIdHeader ?? DEFAULT_REQUEST_ID_HEADER;
|
|
5031
5125
|
if (requestIdHeader !== false && (typeof requestIdHeader !== "string" || !isHeaderName(requestIdHeader))) throw invalidOption("requestIdHeader", "a header name or false", requestIdHeader);
|
|
5032
|
-
const generateRequestId = options.generateRequestId ??
|
|
5126
|
+
const generateRequestId = options.generateRequestId ?? randomRequestId;
|
|
5033
5127
|
if (typeof generateRequestId !== "function") throw invalidOption("generateRequestId", "a function", generateRequestId);
|
|
5034
5128
|
const adapter = options.adapter ?? nodeAdapter();
|
|
5035
5129
|
if (adapter === null || typeof adapter !== "object" || typeof adapter.registerRoute !== "function" || typeof adapter.listen !== "function" || typeof adapter.inject !== "function") throw invalidOption("adapter", "an HttpAdapter (nodeAdapter(), fastifyAdapter()...)", adapter);
|