@zudojs/http 1.2.0 → 1.4.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 +221 -0
- package/dist/httpAdapter/http.adapters.d.ts +24 -3
- package/dist/httpAdapter/http.adapters.js +20 -11
- package/dist/httpAdapter/node/httpNode.adapter.d.ts +2 -1
- package/dist/httpAdapter/node/httpNode.adapter.js +17 -2
- package/dist/httpAdapter/node/httpNode.request.js +7 -1
- package/dist/httpAdapter/node/httpNode.type.d.ts +14 -0
- package/dist/httpAgent/http.agent.d.ts +22 -3
- package/dist/httpAgent/http.agent.js +52 -14
- package/dist/httpCacheControl/httpCacheControl.freshness.d.ts +7 -1
- package/dist/httpCacheControl/httpCacheControl.freshness.js +30 -3
- package/dist/httpClient/httpClient.retry.d.ts +17 -12
- package/dist/httpClient/httpClient.retry.js +35 -10
- package/dist/httpClient/httpClient.type.d.ts +14 -0
- package/dist/httpErrors/httpError.base.js +2 -2
- package/dist/httpErrors/httpError.util.d.ts +8 -0
- package/dist/httpErrors/httpError.util.js +12 -0
- package/dist/httpFetchMount/httpFetchMount.core.d.ts +25 -0
- package/dist/httpFetchMount/httpFetchMount.core.js +84 -0
- package/dist/httpFetchMount/httpFetchMount.request.d.ts +21 -0
- package/dist/httpFetchMount/httpFetchMount.request.js +100 -0
- package/dist/httpFetchMount/httpFetchMount.type.d.ts +56 -0
- package/dist/httpFetchMount/httpFetchMount.type.js +5 -0
- package/dist/httpFetchMount/index.d.ts +11 -0
- package/dist/httpFetchMount/index.js +10 -0
- package/dist/httpKeepAlive/httpKeepAlive.core.js +14 -2
- package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.d.ts +18 -0
- package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.js +17 -1
- package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.d.ts +6 -3
- package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.js +34 -6
- package/dist/httpMiddleware/builtin/security/httpMiddleware.security.d.ts +11 -0
- package/dist/httpMiddleware/builtin/security/httpMiddleware.security.js +25 -12
- package/dist/httpMiddleware/httpMiddleware.type.d.ts +9 -1
- package/dist/httpMiddleware/pipeline/httpPipeline.execution.js +22 -45
- package/dist/httpMiddleware/pipeline/httpPipeline.guardResponse.d.ts +36 -0
- package/dist/httpMiddleware/pipeline/httpPipeline.guardResponse.js +57 -0
- package/dist/httpMiddleware/pipeline/httpPipeline.helper.d.ts +2 -1
- package/dist/httpMiddleware/pipeline/httpPipeline.helper.js +10 -0
- package/dist/httpMiddleware/pipeline/index.d.ts +1 -0
- package/dist/httpMiddleware/pipeline/index.js +1 -0
- package/dist/httpNegotiation/httpNegotiation.core.d.ts +13 -0
- package/dist/httpNegotiation/httpNegotiation.core.js +60 -8
- package/dist/httpOpenApi/httpOpenApi.document.d.ts +44 -0
- package/dist/httpOpenApi/httpOpenApi.document.js +61 -0
- package/dist/httpOpenApi/httpOpenApi.mount.d.ts +31 -0
- package/dist/httpOpenApi/httpOpenApi.mount.js +58 -0
- package/dist/httpOpenApi/httpOpenApi.type.d.ts +54 -0
- package/dist/httpOpenApi/httpOpenApi.type.js +5 -0
- package/dist/httpOpenApi/index.d.ts +13 -0
- package/dist/httpOpenApi/index.js +12 -0
- package/dist/httpOpenApi/routeTable/index.d.ts +11 -0
- package/dist/httpOpenApi/routeTable/index.js +11 -0
- package/dist/httpOpenApi/routeTable/routeTable.collect.d.ts +19 -0
- package/dist/httpOpenApi/routeTable/routeTable.collect.js +89 -0
- package/dist/httpOpenApi/routeTable/routeTable.merge.d.ts +15 -0
- package/dist/httpOpenApi/routeTable/routeTable.merge.js +37 -0
- package/dist/httpOpenApi/routeTable/routeTable.template.d.ts +30 -0
- package/dist/httpOpenApi/routeTable/routeTable.template.js +67 -0
- package/dist/httpProxy/http.proxy.d.ts +9 -0
- package/dist/httpProxy/http.proxy.js +35 -3
- package/dist/httpQuery/index.d.ts +11 -2
- package/dist/httpQuery/index.js +11 -2
- package/dist/httpQuery/queryParse/index.d.ts +10 -0
- package/dist/httpQuery/queryParse/index.js +10 -0
- package/dist/httpQuery/queryParse/queryParse.flat.d.ts +13 -0
- package/dist/httpQuery/queryParse/queryParse.flat.js +44 -0
- package/dist/httpQuery/queryParse/queryParse.nested.d.ts +25 -0
- package/dist/httpQuery/queryParse/queryParse.nested.js +112 -0
- package/dist/httpQuery/queryParse/queryParse.tokenizer.d.ts +37 -0
- package/dist/httpQuery/queryParse/queryParse.tokenizer.js +95 -0
- package/dist/httpQuery/queryRequest/index.d.ts +9 -0
- package/dist/httpQuery/queryRequest/index.js +9 -0
- package/dist/httpQuery/queryRequest/query.request.d.ts +43 -0
- package/dist/httpQuery/queryRequest/query.request.js +96 -0
- package/dist/httpQuery/querySerialize/index.d.ts +10 -0
- package/dist/httpQuery/querySerialize/index.js +10 -0
- package/dist/httpQuery/querySerialize/query.util.d.ts +21 -0
- package/dist/httpQuery/querySerialize/query.util.js +67 -0
- package/dist/httpQuery/querySerialize/querySerialize.core.d.ts +12 -0
- package/dist/httpQuery/querySerialize/querySerialize.core.js +97 -0
- package/dist/httpQuery/queryTypes/index.d.ts +11 -0
- package/dist/httpQuery/queryTypes/index.js +9 -0
- package/dist/httpQuery/queryTypes/query.container.d.ts +16 -0
- package/dist/httpQuery/queryTypes/query.container.js +51 -0
- package/dist/httpQuery/queryTypes/query.limit.d.ts +25 -0
- package/dist/httpQuery/queryTypes/query.limit.js +32 -0
- package/dist/httpQuery/queryTypes/query.type.d.ts +62 -0
- package/dist/httpQuery/queryTypes/query.type.js +2 -0
- package/dist/httpRedirect/http.redirect.d.ts +6 -0
- package/dist/httpRedirect/http.redirect.js +53 -2
- package/dist/httpRequest/http.request.d.ts +61 -2
- package/dist/httpRequest/http.request.js +86 -35
- package/dist/httpRequest/httpRequest.context.d.ts +8 -0
- package/dist/httpRequest/httpRequest.context.js +23 -18
- package/dist/httpRequest/index.d.ts +1 -0
- package/dist/httpRequest/index.js +1 -0
- package/dist/httpRequest/requestId/httpRequest.requestId.d.ts +25 -0
- package/dist/httpRequest/requestId/httpRequest.requestId.js +34 -0
- package/dist/httpRequest/requestId/index.d.ts +7 -0
- package/dist/httpRequest/requestId/index.js +7 -0
- package/dist/httpRequest/target/httpRequest.target.d.ts +2 -2
- package/dist/httpRequest/target/httpRequest.target.js +23 -5
- package/dist/httpResponse/httpResponse.writer.js +15 -0
- package/dist/httpRouter/core/factory/httpRoute.factory.base.d.ts +24 -4
- package/dist/httpRouter/core/factory/httpRoute.factory.base.js +73 -17
- package/dist/httpRouter/core/factory/httpRoute.factory.js +3 -3
- package/dist/httpRouter/core/group/httpRouterGroup.core.js +13 -0
- package/dist/httpRouter/core/register/httpRouter.register.js +19 -21
- package/dist/httpRouter/core/types/httpRouter.type.d.ts +37 -1
- package/dist/httpRouter/core/util/httpRoute.util.d.ts +55 -0
- package/dist/httpRouter/core/util/httpRoute.util.js +101 -4
- package/dist/httpRouter/dispatch/httpRoute.dispatcher.d.ts +11 -0
- package/dist/httpRouter/dispatch/httpRoute.dispatcher.js +25 -6
- package/dist/httpRouter/matching/httpRoute.matcher.core.js +51 -6
- package/dist/httpRouter/matching/httpRoute.matcher.d.ts +0 -1
- package/dist/httpRouter/matching/httpRoute.matcher.js +64 -26
- package/dist/httpRouter/pattern/httpRoute.pattern.parse.d.ts +16 -0
- package/dist/httpRouter/pattern/httpRoute.pattern.parse.js +40 -11
- package/dist/httpRouter/pattern/index.d.ts +1 -1
- package/dist/httpRouter/pattern/index.js +1 -1
- package/dist/httpSecurity/httpSecurity.config.js +4 -1
- package/dist/httpSecurity/httpSecurity.validator.js +16 -7
- package/dist/httpServer/factory/httpServer.factory.d.ts +10 -9
- package/dist/httpServer/factory/httpServer.factory.js +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +11 -8
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* The context is intentionally framework-agnostic so adapters can populate it
|
|
8
8
|
* from Node.js, Bun, Deno, or another HTTP runtime.
|
|
9
9
|
*/
|
|
10
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
11
|
import { parseRequestTarget } from "./target/httpRequest.target.js";
|
|
11
12
|
/* -------------------------------------------------------------------------- */
|
|
12
13
|
/* Constants */
|
|
@@ -44,6 +45,7 @@ export class HttpRequestContext {
|
|
|
44
45
|
protocolValue;
|
|
45
46
|
hostnameValue;
|
|
46
47
|
portValue;
|
|
48
|
+
signalValue;
|
|
47
49
|
constructor(init) {
|
|
48
50
|
this.id = init.id ?? generateRequestId();
|
|
49
51
|
this.method = normalizeMethod(init.method);
|
|
@@ -60,6 +62,16 @@ export class HttpRequestContext {
|
|
|
60
62
|
this.protocolValue = init.protocol;
|
|
61
63
|
this.hostnameValue = init.hostname;
|
|
62
64
|
this.portValue = init.port;
|
|
65
|
+
this.signalValue = init.signal;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Aborts when the request is abandoned — for the Node adapter, when the
|
|
69
|
+
* client disconnects before the response finished. `RequestContextInit`
|
|
70
|
+
* always accepted a `signal`, but the constructor dropped it, so the
|
|
71
|
+
* router handed every handler a signal that could never fire.
|
|
72
|
+
*/
|
|
73
|
+
get signal() {
|
|
74
|
+
return this.signalValue;
|
|
63
75
|
}
|
|
64
76
|
/* ------------------------------------------------------------------------ */
|
|
65
77
|
/* Headers */
|
|
@@ -289,6 +301,7 @@ export class HttpRequestContext {
|
|
|
289
301
|
protocol: this.protocolValue,
|
|
290
302
|
hostname: this.hostnameValue,
|
|
291
303
|
port: this.portValue,
|
|
304
|
+
signal: this.signalValue,
|
|
292
305
|
state: {
|
|
293
306
|
...Object.fromEntries(this.stateMap),
|
|
294
307
|
},
|
|
@@ -378,24 +391,16 @@ function validateHeaderValue(value) {
|
|
|
378
391
|
throw new TypeError("HTTP header value cannot contain CR or LF characters.");
|
|
379
392
|
}
|
|
380
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* Builds the request-context store.
|
|
396
|
+
*
|
|
397
|
+
* `AsyncLocalStorage` used to be reached through `globalThis.require`, which
|
|
398
|
+
* does not exist in ESM under Node. The `typeof` guard turned that into a
|
|
399
|
+
* silent `undefined`, so `runWithRequestContext` merely called its callback
|
|
400
|
+
* and `getCurrentRequestContext` always returned `undefined`. It is imported
|
|
401
|
+
* statically now, as the rest of the package imports its Node built-ins.
|
|
402
|
+
*/
|
|
381
403
|
function createAsyncContextStorage() {
|
|
382
|
-
|
|
383
|
-
* AsyncLocalStorage is intentionally loaded lazily so the HTTP package
|
|
384
|
-
* remains usable in browser and non-Node runtimes.
|
|
385
|
-
*/
|
|
386
|
-
try {
|
|
387
|
-
const runtimeRequire = globalThis.require;
|
|
388
|
-
if (typeof runtimeRequire !== "function") {
|
|
389
|
-
return undefined;
|
|
390
|
-
}
|
|
391
|
-
const asyncHooks = runtimeRequire("node:async_hooks");
|
|
392
|
-
if (!asyncHooks.AsyncLocalStorage) {
|
|
393
|
-
return undefined;
|
|
394
|
-
}
|
|
395
|
-
return new asyncHooks.AsyncLocalStorage();
|
|
396
|
-
}
|
|
397
|
-
catch {
|
|
398
|
-
return undefined;
|
|
399
|
-
}
|
|
404
|
+
return new AsyncLocalStorage();
|
|
400
405
|
}
|
|
401
406
|
//# sourceMappingURL=httpRequest.context.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incoming request id handling.
|
|
3
|
+
*
|
|
4
|
+
* @module httpRequest/requestId
|
|
5
|
+
*/
|
|
6
|
+
/** Longest incoming `x-request-id` value reused as `request.id`. */
|
|
7
|
+
export declare const MAX_INCOMING_REQUEST_ID_LENGTH = 128;
|
|
8
|
+
/**
|
|
9
|
+
* Characters an incoming request id may contain: letters, digits and
|
|
10
|
+
* `.`, `_`, `:`, `-`. That covers UUIDs, ULIDs, W3C trace ids and the
|
|
11
|
+
* `service:counter` style many proxies emit, and rules out spaces, quotes,
|
|
12
|
+
* control characters and anything else that could forge a log field.
|
|
13
|
+
*/
|
|
14
|
+
export declare const INCOMING_REQUEST_ID_PATTERN: RegExp;
|
|
15
|
+
/**
|
|
16
|
+
* Returns the incoming request id when it is safe to reuse, otherwise
|
|
17
|
+
* `undefined` (the caller then generates one).
|
|
18
|
+
*
|
|
19
|
+
* The header is client-controlled, so it is only trusted when it is 1 to
|
|
20
|
+
* {@link MAX_INCOMING_REQUEST_ID_LENGTH} characters from
|
|
21
|
+
* {@link INCOMING_REQUEST_ID_PATTERN}. A header sent more than once (joined
|
|
22
|
+
* with `", "` by Node) fails the pattern and is ignored.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveIncomingRequestId(value: string | readonly string[] | undefined): string | undefined;
|
|
25
|
+
//# sourceMappingURL=httpRequest.requestId.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incoming request id handling.
|
|
3
|
+
*
|
|
4
|
+
* @module httpRequest/requestId
|
|
5
|
+
*/
|
|
6
|
+
/** Longest incoming `x-request-id` value reused as `request.id`. */
|
|
7
|
+
export const MAX_INCOMING_REQUEST_ID_LENGTH = 128;
|
|
8
|
+
/**
|
|
9
|
+
* Characters an incoming request id may contain: letters, digits and
|
|
10
|
+
* `.`, `_`, `:`, `-`. That covers UUIDs, ULIDs, W3C trace ids and the
|
|
11
|
+
* `service:counter` style many proxies emit, and rules out spaces, quotes,
|
|
12
|
+
* control characters and anything else that could forge a log field.
|
|
13
|
+
*/
|
|
14
|
+
export const INCOMING_REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]+$/;
|
|
15
|
+
/**
|
|
16
|
+
* Returns the incoming request id when it is safe to reuse, otherwise
|
|
17
|
+
* `undefined` (the caller then generates one).
|
|
18
|
+
*
|
|
19
|
+
* The header is client-controlled, so it is only trusted when it is 1 to
|
|
20
|
+
* {@link MAX_INCOMING_REQUEST_ID_LENGTH} characters from
|
|
21
|
+
* {@link INCOMING_REQUEST_ID_PATTERN}. A header sent more than once (joined
|
|
22
|
+
* with `", "` by Node) fails the pattern and is ignored.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveIncomingRequestId(value) {
|
|
25
|
+
if (typeof value !== "string") {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const id = value.trim();
|
|
29
|
+
if (id.length === 0 || id.length > MAX_INCOMING_REQUEST_ID_LENGTH) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return INCOMING_REQUEST_ID_PATTERN.test(id) ? id : undefined;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=httpRequest.requestId.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/http/httpRequest/requestId
|
|
3
|
+
*
|
|
4
|
+
* Validation of an incoming `x-request-id` before it becomes `request.id`.
|
|
5
|
+
*/
|
|
6
|
+
export { MAX_INCOMING_REQUEST_ID_LENGTH, INCOMING_REQUEST_ID_PATTERN, resolveIncomingRequestId, } from "./httpRequest.requestId.js";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/http/httpRequest/requestId
|
|
3
|
+
*
|
|
4
|
+
* Validation of an incoming `x-request-id` before it becomes `request.id`.
|
|
5
|
+
*/
|
|
6
|
+
export { MAX_INCOMING_REQUEST_ID_LENGTH, INCOMING_REQUEST_ID_PATTERN, resolveIncomingRequestId, } from "./httpRequest.requestId.js";
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
/**
|
|
18
18
|
* Parses a request-target (origin-form, absolute-form or `*`) into a URL.
|
|
19
19
|
*
|
|
20
|
-
* An origin-form target is never parsed as an authority
|
|
21
|
-
* yields the root URL.
|
|
20
|
+
* An origin-form target is never parsed as an authority, and repeated slashes
|
|
21
|
+
* in the path are collapsed. Unparseable input yields the root URL.
|
|
22
22
|
*/
|
|
23
23
|
export declare function parseRequestTarget(target: string): URL;
|
|
24
24
|
/**
|
|
@@ -17,21 +17,39 @@
|
|
|
17
17
|
const TARGET_BASE = "http://zudojs.invalid";
|
|
18
18
|
const ABSOLUTE_FORM = /^https?:\/\//i;
|
|
19
19
|
const ENCODED_DOT = /%2e/gi;
|
|
20
|
+
const REPEATED_SLASH = /\/{2,}/g;
|
|
21
|
+
/**
|
|
22
|
+
* Collapses repeated slashes in a parsed target's path.
|
|
23
|
+
*
|
|
24
|
+
* The router normalises `/{2,}` away before matching, while the request
|
|
25
|
+
* context kept them, so `//admin/secret` dispatched to the route registered
|
|
26
|
+
* at `/admin/secret` while a guard reading `request.path` saw a path that did
|
|
27
|
+
* not start with `/admin/`. Both sides parse here, so collapsing once here
|
|
28
|
+
* keeps them in agreement. The query and fragment are untouched.
|
|
29
|
+
*/
|
|
30
|
+
function collapsePathSlashes(url) {
|
|
31
|
+
if (!url.pathname.includes("//")) {
|
|
32
|
+
return url;
|
|
33
|
+
}
|
|
34
|
+
const collapsed = new URL(url.href);
|
|
35
|
+
collapsed.pathname = url.pathname.replace(REPEATED_SLASH, "/");
|
|
36
|
+
return collapsed;
|
|
37
|
+
}
|
|
20
38
|
/**
|
|
21
39
|
* Parses a request-target (origin-form, absolute-form or `*`) into a URL.
|
|
22
40
|
*
|
|
23
|
-
* An origin-form target is never parsed as an authority
|
|
24
|
-
* yields the root URL.
|
|
41
|
+
* An origin-form target is never parsed as an authority, and repeated slashes
|
|
42
|
+
* in the path are collapsed. Unparseable input yields the root URL.
|
|
25
43
|
*/
|
|
26
44
|
export function parseRequestTarget(target) {
|
|
27
45
|
try {
|
|
28
46
|
if (target.startsWith("/")) {
|
|
29
|
-
return new URL(`${TARGET_BASE}${target}`);
|
|
47
|
+
return collapsePathSlashes(new URL(`${TARGET_BASE}${target}`));
|
|
30
48
|
}
|
|
31
49
|
if (ABSOLUTE_FORM.test(target)) {
|
|
32
|
-
return new URL(target);
|
|
50
|
+
return collapsePathSlashes(new URL(target));
|
|
33
51
|
}
|
|
34
|
-
return new URL(`${TARGET_BASE}/${target === "*" ? "" : target}`);
|
|
52
|
+
return collapsePathSlashes(new URL(`${TARGET_BASE}/${target === "*" ? "" : target}`));
|
|
35
53
|
}
|
|
36
54
|
catch {
|
|
37
55
|
return new URL(`${TARGET_BASE}/`);
|
|
@@ -183,10 +183,21 @@ export function normalizeBody(body) {
|
|
|
183
183
|
/* -------------------------------------------------------------------------- */
|
|
184
184
|
export async function writeReadableStream(stream, writer) {
|
|
185
185
|
const reader = stream.getReader();
|
|
186
|
+
let finished = false;
|
|
186
187
|
try {
|
|
187
188
|
while (true) {
|
|
189
|
+
/*
|
|
190
|
+
* A sink that stopped accepting data (the client disconnected) will
|
|
191
|
+
* never drain. Without this check an unbounded stream — server-sent
|
|
192
|
+
* events, a proxied download — kept being pulled into a dead socket
|
|
193
|
+
* for as long as its source produced.
|
|
194
|
+
*/
|
|
195
|
+
if (writer.writable === false) {
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
188
198
|
const result = await reader.read();
|
|
189
199
|
if (result.done) {
|
|
200
|
+
finished = true;
|
|
190
201
|
break;
|
|
191
202
|
}
|
|
192
203
|
if (result.value) {
|
|
@@ -198,6 +209,10 @@ export async function writeReadableStream(stream, writer) {
|
|
|
198
209
|
}
|
|
199
210
|
}
|
|
200
211
|
finally {
|
|
212
|
+
if (!finished) {
|
|
213
|
+
/* Tell the source to stop producing; its failure is not ours. */
|
|
214
|
+
await reader.cancel().catch(() => undefined);
|
|
215
|
+
}
|
|
201
216
|
reader.releaseLock();
|
|
202
217
|
}
|
|
203
218
|
}
|
|
@@ -4,12 +4,23 @@
|
|
|
4
4
|
* Internal normalization, validation, execution, and response utilities for
|
|
5
5
|
* route creation and dispatch.
|
|
6
6
|
*/
|
|
7
|
-
import type { HttpMethod, MatchedRoute, CompiledRoute, HttpRouterContext, HttpRouterRequestContext } from "../types/httpRouter.type.js";
|
|
7
|
+
import type { HttpMethod, MatchedRoute, CompiledRoute, HttpRouterContext, HttpRouterRequestContext, RouterHandlerResult } from "../types/httpRouter.type.js";
|
|
8
8
|
import { type HttpResponseContext as ResponseContext } from "../../../httpResponse/httpResponse.context.js";
|
|
9
9
|
export declare function normalizeMethod(method: string): HttpMethod | "*";
|
|
10
10
|
export declare function normalizeMethods(method: HttpMethod | readonly HttpMethod[] | "*"): readonly (HttpMethod | "*")[];
|
|
11
11
|
export declare function isHttpMethod(value: string): value is HttpMethod;
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Collects the methods registered for a path.
|
|
14
|
+
*
|
|
15
|
+
* @param routes - The compiled routes to consider.
|
|
16
|
+
* @param path - The request path.
|
|
17
|
+
* @param caseSensitive - The router's case sensitivity. This used to be
|
|
18
|
+
* hardcoded to `false`, so a case-sensitive router advertised `Allow`
|
|
19
|
+
* methods belonging to a route that only differed by case — a method the
|
|
20
|
+
* client would then get a 404 from, and a disclosure of the other route.
|
|
21
|
+
* @returns The allowed methods, with `HEAD` implied by `GET`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function collectAllowedMethods(routes: readonly CompiledRoute[], path: string, caseSensitive?: boolean): HttpMethod[];
|
|
13
24
|
/**
|
|
14
25
|
* Extracts the monotonic registration sequence from a generated route id.
|
|
15
26
|
*
|
|
@@ -22,9 +33,12 @@ export declare function extractRouteSequence(id: string): number;
|
|
|
22
33
|
*/
|
|
23
34
|
export declare function createFallbackRoute(path: string, method: string): MatchedRoute;
|
|
24
35
|
/**
|
|
25
|
-
* Coerces a handler result into a response context
|
|
36
|
+
* Coerces a handler result into a response context: a response context or
|
|
37
|
+
* web `Response` as built, `undefined`/`null` as `204`, and any other value
|
|
38
|
+
* as a `200` JSON body, the way server handlers treat a plain value. A
|
|
39
|
+
* plain object used to be dropped for an empty `204`.
|
|
26
40
|
*/
|
|
27
|
-
export declare function normalizeResponse(value:
|
|
41
|
+
export declare function normalizeResponse(value: RouterHandlerResult): Promise<ResponseContext>;
|
|
28
42
|
/**
|
|
29
43
|
* Builds the automatic `OPTIONS` response for a matched path.
|
|
30
44
|
*/
|
|
@@ -39,6 +53,12 @@ export declare function defaultNotFoundHandler(context: HttpRouterRequestContext
|
|
|
39
53
|
export declare function defaultMethodNotAllowedHandler(context: HttpRouterRequestContext, allowedMethods: readonly HttpMethod[]): ResponseContext;
|
|
40
54
|
/**
|
|
41
55
|
* Runs a matched route's middleware chain followed by its handler.
|
|
56
|
+
*
|
|
57
|
+
* Every result is folded into the ambient response context
|
|
58
|
+
* (`context.middleware.response`), which is the object route middleware
|
|
59
|
+
* writes to. Returning the handler's brand new response instead — as this
|
|
60
|
+
* used to — silently discarded every header, cookie and status a route
|
|
61
|
+
* middleware had set before calling `next()`.
|
|
42
62
|
*/
|
|
43
63
|
export declare function executeRoute(route: MatchedRoute, context: HttpRouterContext): Promise<ResponseContext>;
|
|
44
64
|
//# sourceMappingURL=httpRoute.factory.base.d.ts.map
|
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* Internal normalization, validation, execution, and response utilities for
|
|
5
5
|
* route creation and dispatch.
|
|
6
6
|
*/
|
|
7
|
+
import { isGuardResponse } from "@zudojs/middleware";
|
|
7
8
|
import { HttpRouterError } from "../error/httpRouter.error.js";
|
|
8
9
|
import { formatAllowHeader } from "../../../httpMethods/http.methods.js";
|
|
9
10
|
import { matchCompiledRoute } from "../../matching/httpRoute.matcher.core.js";
|
|
10
11
|
import { HttpResponseContext, } from "../../../httpResponse/httpResponse.context.js";
|
|
12
|
+
import { bufferWebResponse } from "../../../httpResponse/httpResponse.fromWeb.js";
|
|
13
|
+
import { applyGuardResponse } from "../../../httpMiddleware/pipeline/httpPipeline.guardResponse.js";
|
|
11
14
|
/* -------------------------------------------------------------------------- */
|
|
12
15
|
/* Method Helpers */
|
|
13
16
|
/* -------------------------------------------------------------------------- */
|
|
@@ -36,10 +39,21 @@ export function isHttpMethod(value) {
|
|
|
36
39
|
value === "CONNECT" ||
|
|
37
40
|
value === "TRACE");
|
|
38
41
|
}
|
|
39
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Collects the methods registered for a path.
|
|
44
|
+
*
|
|
45
|
+
* @param routes - The compiled routes to consider.
|
|
46
|
+
* @param path - The request path.
|
|
47
|
+
* @param caseSensitive - The router's case sensitivity. This used to be
|
|
48
|
+
* hardcoded to `false`, so a case-sensitive router advertised `Allow`
|
|
49
|
+
* methods belonging to a route that only differed by case — a method the
|
|
50
|
+
* client would then get a 404 from, and a disclosure of the other route.
|
|
51
|
+
* @returns The allowed methods, with `HEAD` implied by `GET`.
|
|
52
|
+
*/
|
|
53
|
+
export function collectAllowedMethods(routes, path, caseSensitive = false) {
|
|
40
54
|
const methods = new Set();
|
|
41
55
|
for (const route of routes) {
|
|
42
|
-
if (!matchCompiledRoute(route, path,
|
|
56
|
+
if (!matchCompiledRoute(route, path, caseSensitive)) {
|
|
43
57
|
continue;
|
|
44
58
|
}
|
|
45
59
|
if (isHttpMethod(route.definition.method)) {
|
|
@@ -88,24 +102,27 @@ export function createFallbackRoute(path, method) {
|
|
|
88
102
|
/* Response Helpers */
|
|
89
103
|
/* -------------------------------------------------------------------------- */
|
|
90
104
|
/**
|
|
91
|
-
* Coerces a handler result into a response context
|
|
105
|
+
* Coerces a handler result into a response context: a response context or
|
|
106
|
+
* web `Response` as built, `undefined`/`null` as `204`, and any other value
|
|
107
|
+
* as a `200` JSON body, the way server handlers treat a plain value. A
|
|
108
|
+
* plain object used to be dropped for an empty `204`.
|
|
92
109
|
*/
|
|
93
110
|
export async function normalizeResponse(value) {
|
|
94
111
|
if (value instanceof HttpResponseContext) {
|
|
95
112
|
return value;
|
|
96
113
|
}
|
|
97
114
|
if (typeof Response !== "undefined" && value instanceof Response) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
headers: Object.fromEntries(value.headers.entries()),
|
|
105
|
-
body,
|
|
106
|
-
});
|
|
115
|
+
/*
|
|
116
|
+
* `Object.fromEntries(headers.entries())` folded every `Set-Cookie` into
|
|
117
|
+
* one comma-joined value, which browsers read as a single malformed
|
|
118
|
+
* cookie. `bufferWebResponse` keeps each cookie separate.
|
|
119
|
+
*/
|
|
120
|
+
return bufferWebResponse(value);
|
|
107
121
|
}
|
|
108
|
-
|
|
122
|
+
if (value === undefined || value === null) {
|
|
123
|
+
return new HttpResponseContext({ status: 204 });
|
|
124
|
+
}
|
|
125
|
+
return new HttpResponseContext({ status: 200 }).json(value);
|
|
109
126
|
}
|
|
110
127
|
/**
|
|
111
128
|
* Builds the automatic `OPTIONS` response for a matched path.
|
|
@@ -153,11 +170,43 @@ export function defaultMethodNotAllowedHandler(context, allowedMethods) {
|
|
|
153
170
|
/* -------------------------------------------------------------------------- */
|
|
154
171
|
/* Route Execution */
|
|
155
172
|
/* -------------------------------------------------------------------------- */
|
|
173
|
+
/**
|
|
174
|
+
* Merges one response context into another.
|
|
175
|
+
*
|
|
176
|
+
* Status, status text, headers, cookies, metadata and body are all carried
|
|
177
|
+
* over, so nothing a handler produced is lost.
|
|
178
|
+
*
|
|
179
|
+
* @param target - The response that stays authoritative.
|
|
180
|
+
* @param source - The response to fold into it.
|
|
181
|
+
* @returns The target response.
|
|
182
|
+
*/
|
|
183
|
+
function mergeRouteResponse(target, source) {
|
|
184
|
+
if (source === target) {
|
|
185
|
+
return target;
|
|
186
|
+
}
|
|
187
|
+
target.setStatus(source.status, source.statusText);
|
|
188
|
+
target.headers_obj(source.headers);
|
|
189
|
+
for (const cookie of source.cookies) {
|
|
190
|
+
target.setCookie(cookie);
|
|
191
|
+
}
|
|
192
|
+
for (const [key, value] of Object.entries(source.metadata)) {
|
|
193
|
+
target.setMetadata(key, value);
|
|
194
|
+
}
|
|
195
|
+
target.setBody(source.body);
|
|
196
|
+
return target;
|
|
197
|
+
}
|
|
156
198
|
/**
|
|
157
199
|
* Runs a matched route's middleware chain followed by its handler.
|
|
200
|
+
*
|
|
201
|
+
* Every result is folded into the ambient response context
|
|
202
|
+
* (`context.middleware.response`), which is the object route middleware
|
|
203
|
+
* writes to. Returning the handler's brand new response instead — as this
|
|
204
|
+
* used to — silently discarded every header, cookie and status a route
|
|
205
|
+
* middleware had set before calling `next()`.
|
|
158
206
|
*/
|
|
159
207
|
export async function executeRoute(route, context) {
|
|
160
208
|
const layers = route.middleware;
|
|
209
|
+
const ambient = context.middleware.response;
|
|
161
210
|
let invoked = -1;
|
|
162
211
|
const run = async (index) => {
|
|
163
212
|
if (index <= invoked) {
|
|
@@ -166,7 +215,7 @@ export async function executeRoute(route, context) {
|
|
|
166
215
|
invoked = index;
|
|
167
216
|
const layer = layers[index];
|
|
168
217
|
if (layer === undefined) {
|
|
169
|
-
return normalizeResponse(await route.handler(context));
|
|
218
|
+
return mergeRouteResponse(ambient, await normalizeResponse(await route.handler(context)));
|
|
170
219
|
}
|
|
171
220
|
let downstream;
|
|
172
221
|
const result = await layer(context.middleware, async () => {
|
|
@@ -174,12 +223,19 @@ export async function executeRoute(route, context) {
|
|
|
174
223
|
return downstream;
|
|
175
224
|
});
|
|
176
225
|
if (result instanceof HttpResponseContext) {
|
|
177
|
-
return result;
|
|
226
|
+
return mergeRouteResponse(ambient, result);
|
|
227
|
+
}
|
|
228
|
+
/*
|
|
229
|
+
* A guard (permissions, tenancy) refusing the request. Before this was
|
|
230
|
+
* honoured the returned object was ignored and the ambient 200 went out.
|
|
231
|
+
*/
|
|
232
|
+
if (isGuardResponse(result)) {
|
|
233
|
+
return applyGuardResponse(ambient, result);
|
|
178
234
|
}
|
|
179
235
|
if (typeof Response !== "undefined" && result instanceof Response) {
|
|
180
|
-
return normalizeResponse(result);
|
|
236
|
+
return mergeRouteResponse(ambient, await normalizeResponse(result));
|
|
181
237
|
}
|
|
182
|
-
return downstream ??
|
|
238
|
+
return downstream ?? ambient;
|
|
183
239
|
};
|
|
184
240
|
return run(0);
|
|
185
241
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { HttpRouter } from "../register/httpRouter.register.js";
|
|
5
5
|
import { HttpRouterGroup } from "../group/httpRouterGroup.core.js";
|
|
6
6
|
import { RouteConflictError, InvalidRoutePatternError, } from "../error/httpRouter.error.js";
|
|
7
|
-
import {
|
|
7
|
+
import { normalizeRoutePattern } from "../util/httpRoute.util.js";
|
|
8
8
|
/**
|
|
9
9
|
* Creates a new HTTP router instance.
|
|
10
10
|
*/
|
|
@@ -17,7 +17,7 @@ export function createRouter(options = {}) {
|
|
|
17
17
|
export function createRoute(definition) {
|
|
18
18
|
return {
|
|
19
19
|
...definition,
|
|
20
|
-
path:
|
|
20
|
+
path: normalizeRoutePattern(definition.path),
|
|
21
21
|
middleware: Object.freeze([...(definition.middleware ?? [])]),
|
|
22
22
|
metadata: Object.freeze({ ...(definition.metadata ?? {}) }),
|
|
23
23
|
};
|
|
@@ -26,7 +26,7 @@ export function createRoute(definition) {
|
|
|
26
26
|
* Builds a route path from a pattern and parameters.
|
|
27
27
|
*/
|
|
28
28
|
export function buildRoutePath(pattern, params = {}) {
|
|
29
|
-
const normalized =
|
|
29
|
+
const normalized = normalizeRoutePattern(pattern);
|
|
30
30
|
return normalized
|
|
31
31
|
.replace(/:([a-zA-Z_][a-zA-Z0-9_-]*)(\?)?/g, (_match, name, optional) => {
|
|
32
32
|
const value = params[name];
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Registers routes against a parent router under a shared path prefix and a
|
|
5
5
|
* shared set of default route options.
|
|
6
6
|
*/
|
|
7
|
+
import { mergeRouteOpenAPI } from "../../../httpOpenApi/routeTable/routeTable.merge.js";
|
|
7
8
|
export class HttpRouterGroup {
|
|
8
9
|
router;
|
|
9
10
|
prefix;
|
|
@@ -51,7 +52,15 @@ export class HttpRouterGroup {
|
|
|
51
52
|
return `${left}/${right}` || "/";
|
|
52
53
|
}
|
|
53
54
|
mergeOptions(options) {
|
|
55
|
+
/*
|
|
56
|
+
* `metadata.openapi` is the same setting as `openapi` (the router stores
|
|
57
|
+
* one as the other). Reading only `openapi` let a group's documentation
|
|
58
|
+
* defaults replace a route's `metadata: { openapi: false }`, publishing
|
|
59
|
+
* a route its author had hidden.
|
|
60
|
+
*/
|
|
61
|
+
const openapi = mergeRouteOpenAPI(openAPIOf(this.defaults), openAPIOf(options));
|
|
54
62
|
return {
|
|
63
|
+
...(openapi === undefined ? {} : { openapi }),
|
|
55
64
|
name: options.name ?? this.defaults.name,
|
|
56
65
|
middleware: [
|
|
57
66
|
...(this.defaults.middleware ?? []),
|
|
@@ -65,4 +74,8 @@ export class HttpRouterGroup {
|
|
|
65
74
|
};
|
|
66
75
|
}
|
|
67
76
|
}
|
|
77
|
+
function openAPIOf(options) {
|
|
78
|
+
return options.openapi ??
|
|
79
|
+
options.metadata?.["openapi"];
|
|
80
|
+
}
|
|
68
81
|
//# sourceMappingURL=httpRouterGroup.core.js.map
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
import { HttpRouterError, RouteConflictError, } from "../error/httpRouter.error.js";
|
|
5
5
|
import { HttpRouterGroup } from "../group/httpRouterGroup.core.js";
|
|
6
6
|
import { collectAllowedMethods, createFallbackRoute, createOptionsResponse, defaultMethodNotAllowedHandler, defaultNotFoundHandler, executeRoute, extractRouteSequence, isHttpMethod, normalizeMethod, normalizeMethods, normalizeResponse, } from "../factory/httpRoute.factory.base.js";
|
|
7
|
-
import { getRequestMethod, getRequestSignal, getRequestUrl, normalizePath, parseQuery, parseUrl, } from "../util/httpRoute.util.js";
|
|
7
|
+
import { getRequestMethod, getRequestSignal, applyRouteParams, getRequestUrl, normalizeMatchPath, normalizePath, normalizeRoutePattern, parseQuery, parseUrl, } from "../util/httpRoute.util.js";
|
|
8
8
|
import { matchCompiledRoute } from "../../matching/httpRoute.matcher.core.js";
|
|
9
|
-
import { compileRoute } from "../../pattern/httpRoute.pattern.parse.js";
|
|
9
|
+
import { compareSegmentSpecificity, compileRoute, } from "../../pattern/httpRoute.pattern.parse.js";
|
|
10
10
|
import { createRouterMiddlewareContext } from "../../httpRouter.context.js";
|
|
11
11
|
export class HttpRouter {
|
|
12
12
|
routes = [];
|
|
@@ -36,6 +36,7 @@ export class HttpRouter {
|
|
|
36
36
|
middleware: definition.middleware,
|
|
37
37
|
metadata: definition.metadata,
|
|
38
38
|
strictTrailingSlash: definition.strictTrailingSlash,
|
|
39
|
+
openapi: definition.openapi,
|
|
39
40
|
});
|
|
40
41
|
}
|
|
41
42
|
return () => {
|
|
@@ -90,7 +91,7 @@ export class HttpRouter {
|
|
|
90
91
|
remove(method, path) {
|
|
91
92
|
const normalizedMethod = normalizeMethod(method);
|
|
92
93
|
const index = this.routes.findIndex((route) => route.definition.method === normalizedMethod &&
|
|
93
|
-
route.definition.path ===
|
|
94
|
+
route.definition.path === normalizeRoutePattern(path));
|
|
94
95
|
if (index === -1) {
|
|
95
96
|
return false;
|
|
96
97
|
}
|
|
@@ -122,11 +123,13 @@ export class HttpRouter {
|
|
|
122
123
|
match(method, path) {
|
|
123
124
|
const normalizedMethod = method.toUpperCase();
|
|
124
125
|
const normalizedPath = normalizePath(path);
|
|
126
|
+
const matchPath = normalizeMatchPath(path);
|
|
125
127
|
const candidates = this.sortedRoutes();
|
|
128
|
+
const allowedForPath = () => collectAllowedMethods(candidates, matchPath, this.routerOptions.caseSensitive);
|
|
126
129
|
const allowed = new Set();
|
|
127
130
|
let pathMatched = false;
|
|
128
131
|
for (const route of candidates) {
|
|
129
|
-
const params = matchCompiledRoute(route,
|
|
132
|
+
const params = matchCompiledRoute(route, matchPath, this.routerOptions.caseSensitive);
|
|
130
133
|
if (!params) {
|
|
131
134
|
continue;
|
|
132
135
|
}
|
|
@@ -137,9 +140,7 @@ export class HttpRouter {
|
|
|
137
140
|
matched: true,
|
|
138
141
|
route: route.definition,
|
|
139
142
|
params,
|
|
140
|
-
allowedMethods: Object.freeze([
|
|
141
|
-
...collectAllowedMethods(candidates, normalizedPath),
|
|
142
|
-
]),
|
|
143
|
+
allowedMethods: Object.freeze([...allowedForPath()]),
|
|
143
144
|
path: normalizedPath,
|
|
144
145
|
method: normalizedMethod,
|
|
145
146
|
};
|
|
@@ -153,16 +154,13 @@ export class HttpRouter {
|
|
|
153
154
|
if (route.definition.method !== "GET") {
|
|
154
155
|
continue;
|
|
155
156
|
}
|
|
156
|
-
const params = matchCompiledRoute(route,
|
|
157
|
+
const params = matchCompiledRoute(route, matchPath, this.routerOptions.caseSensitive);
|
|
157
158
|
if (params) {
|
|
158
159
|
return {
|
|
159
160
|
matched: true,
|
|
160
161
|
route: route.definition,
|
|
161
162
|
params,
|
|
162
|
-
allowedMethods: Object.freeze([
|
|
163
|
-
...collectAllowedMethods(candidates, normalizedPath),
|
|
164
|
-
"HEAD",
|
|
165
|
-
]),
|
|
163
|
+
allowedMethods: Object.freeze([...allowedForPath(), "HEAD"]),
|
|
166
164
|
path: normalizedPath,
|
|
167
165
|
method: normalizedMethod,
|
|
168
166
|
};
|
|
@@ -176,10 +174,7 @@ export class HttpRouter {
|
|
|
176
174
|
matched: true,
|
|
177
175
|
route: undefined,
|
|
178
176
|
params: {},
|
|
179
|
-
allowedMethods: Object.freeze([
|
|
180
|
-
...collectAllowedMethods(candidates, normalizedPath),
|
|
181
|
-
"OPTIONS",
|
|
182
|
-
]),
|
|
177
|
+
allowedMethods: Object.freeze([...allowedForPath(), "OPTIONS"]),
|
|
183
178
|
path: normalizedPath,
|
|
184
179
|
method: normalizedMethod,
|
|
185
180
|
};
|
|
@@ -217,6 +212,7 @@ export class HttpRouter {
|
|
|
217
212
|
signal,
|
|
218
213
|
};
|
|
219
214
|
if (match.matched && match.route) {
|
|
215
|
+
applyRouteParams(request, match.params);
|
|
220
216
|
const response = await executeRoute(match.route, routerContext);
|
|
221
217
|
return {
|
|
222
218
|
response: await normalizeResponse(response),
|
|
@@ -261,11 +257,11 @@ export class HttpRouter {
|
|
|
261
257
|
/* ------------------------------------------------------------------------ */
|
|
262
258
|
register(method, path, handler, options) {
|
|
263
259
|
const normalizedMethod = normalizeMethod(method);
|
|
264
|
-
const normalizedPath =
|
|
260
|
+
const normalizedPath = normalizeRoutePattern(path);
|
|
265
261
|
if (typeof handler !== "function") {
|
|
266
262
|
throw new HttpRouterError("Route handler must be a function.");
|
|
267
263
|
}
|
|
268
|
-
const compiled = compileRoute(
|
|
264
|
+
const compiled = compileRoute(path, this.routerOptions.strictTrailingSlash ||
|
|
269
265
|
options.strictTrailingSlash === true);
|
|
270
266
|
const existing = this.routes.find((route) => route.definition.method === normalizedMethod &&
|
|
271
267
|
route.definition.path === normalizedPath);
|
|
@@ -281,6 +277,7 @@ export class HttpRouter {
|
|
|
281
277
|
params: {},
|
|
282
278
|
metadata: Object.freeze({
|
|
283
279
|
...(options.metadata ?? {}),
|
|
280
|
+
...(options.openapi === undefined ? {} : { openapi: options.openapi }),
|
|
284
281
|
}),
|
|
285
282
|
handler,
|
|
286
283
|
middleware: Object.freeze([...(options.middleware ?? [])]),
|
|
@@ -290,6 +287,7 @@ export class HttpRouter {
|
|
|
290
287
|
segments: compiled.segments,
|
|
291
288
|
score: compiled.score,
|
|
292
289
|
strictTrailingSlash: compiled.strictTrailingSlash,
|
|
290
|
+
expectsTrailingSlash: compiled.expectsTrailingSlash,
|
|
293
291
|
});
|
|
294
292
|
return () => {
|
|
295
293
|
this.remove(normalizedMethod, normalizedPath);
|
|
@@ -297,9 +295,9 @@ export class HttpRouter {
|
|
|
297
295
|
}
|
|
298
296
|
sortedRoutes() {
|
|
299
297
|
return [...this.routes].sort((left, right) => {
|
|
300
|
-
const
|
|
301
|
-
if (
|
|
302
|
-
return
|
|
298
|
+
const specificity = compareSegmentSpecificity(left.segments, right.segments);
|
|
299
|
+
if (specificity !== 0) {
|
|
300
|
+
return specificity;
|
|
303
301
|
}
|
|
304
302
|
return (extractRouteSequence(left.definition.id) -
|
|
305
303
|
extractRouteSequence(right.definition.id));
|