@zudojs/http 1.1.0 → 1.3.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 +53 -3
- package/dist/httpAdapter/http.adapter.d.ts +34 -3
- package/dist/httpAdapter/http.adapter.js +22 -6
- package/dist/httpAdapter/http.adapters.d.ts +24 -3
- package/dist/httpAdapter/http.adapters.js +24 -45
- package/dist/httpAdapter/httpAdapter.errorResponse.d.ts +2 -2
- package/dist/httpAdapter/httpAdapter.errorResponse.js +26 -9
- package/dist/httpAdapter/httpAdapter.logger.d.ts +19 -0
- package/dist/httpAdapter/httpAdapter.logger.js +21 -0
- package/dist/httpAdapter/node/httpNode.adapter.d.ts +12 -1
- package/dist/httpAdapter/node/httpNode.adapter.js +28 -14
- package/dist/httpAdapter/node/httpNode.request.d.ts +8 -7
- package/dist/httpAdapter/node/httpNode.request.js +17 -27
- package/dist/httpAdapter/node/httpNode.server.d.ts +6 -0
- package/dist/httpAdapter/node/httpNode.server.js +6 -0
- package/dist/httpAdapter/node/httpNode.type.d.ts +2 -1
- 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/httpCookies/http.cookies.d.ts +29 -2
- package/dist/httpCookies/http.cookies.js +44 -24
- package/dist/httpCookies/httpCookies.defaults.d.ts +40 -0
- package/dist/httpCookies/httpCookies.defaults.js +42 -0
- package/dist/httpCookies/index.d.ts +1 -0
- package/dist/httpCookies/index.js +1 -0
- package/dist/httpKeepAlive/httpKeepAlive.core.js +14 -2
- package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.d.ts +22 -1
- package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.js +19 -4
- package/dist/httpMiddleware/builtin/conditional/index.d.ts +1 -0
- package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.d.ts +8 -0
- package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.js +25 -22
- package/dist/httpMiddleware/builtin/helpers/httpMiddleware.accessor.js +2 -6
- package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.d.ts +33 -0
- package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.js +59 -0
- package/dist/httpMiddleware/builtin/helpers/index.d.ts +1 -0
- package/dist/httpMiddleware/builtin/helpers/index.js +1 -0
- package/dist/httpMiddleware/builtin/image/httpMiddleware.image.d.ts +13 -0
- package/dist/httpMiddleware/builtin/image/httpMiddleware.image.js +31 -53
- package/dist/httpMiddleware/builtin/index.d.ts +1 -0
- package/dist/httpMiddleware/builtin/index.js +1 -0
- 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 +47 -0
- package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.js +61 -0
- package/dist/httpMiddleware/builtin/rateLimit/index.d.ts +7 -0
- package/dist/httpMiddleware/builtin/rateLimit/index.js +7 -0
- package/dist/httpMiddleware/builtin/security/httpMiddleware.security.d.ts +11 -0
- package/dist/httpMiddleware/builtin/security/httpMiddleware.security.js +25 -12
- package/dist/httpMiddleware/builtin/static/httpMiddleware.static.js +18 -4
- package/dist/httpMiddleware/builtin/video/httpMiddleware.video.d.ts +19 -1
- package/dist/httpMiddleware/builtin/video/httpMiddleware.video.js +38 -59
- package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.d.ts +12 -0
- package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.js +31 -0
- package/dist/httpMiddleware/httpMiddleware.error.d.ts +5 -31
- package/dist/httpMiddleware/httpMiddleware.error.js +5 -53
- package/dist/httpNegotiation/httpNegotiation.core.d.ts +13 -0
- package/dist/httpNegotiation/httpNegotiation.core.js +60 -8
- package/dist/httpProxy/http.proxy.d.ts +16 -0
- package/dist/httpProxy/http.proxy.js +44 -3
- package/dist/httpProxy/httpProxy.pathGuard.d.ts +23 -0
- package/dist/httpProxy/httpProxy.pathGuard.js +43 -0
- package/dist/httpProxy/index.d.ts +1 -0
- package/dist/httpProxy/index.js +1 -0
- 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 +7 -0
- package/dist/httpRequest/httpRequest.context.js +26 -34
- package/dist/httpRequest/index.d.ts +1 -0
- package/dist/httpRequest/index.js +1 -0
- package/dist/httpRequest/target/httpRequest.target.d.ts +45 -0
- package/dist/httpRequest/target/httpRequest.target.js +111 -0
- package/dist/httpRequest/target/index.d.ts +9 -0
- package/dist/httpRequest/target/index.js +9 -0
- package/dist/httpResponse/httpResponse.helper.js +2 -1
- package/dist/httpRouter/core/factory/httpRoute.factory.base.d.ts +18 -1
- package/dist/httpRouter/core/factory/httpRoute.factory.base.js +49 -6
- package/dist/httpRouter/core/factory/httpRoute.factory.js +3 -3
- package/dist/httpRouter/core/register/httpRouter.register.js +16 -21
- package/dist/httpRouter/core/types/httpRouter.type.d.ts +6 -0
- package/dist/httpRouter/core/util/httpRoute.util.d.ts +61 -0
- package/dist/httpRouter/core/util/httpRoute.util.js +102 -17
- package/dist/httpRouter/dispatch/httpRoute.dispatcher.d.ts +11 -0
- package/dist/httpRouter/dispatch/httpRoute.dispatcher.js +5 -3
- 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.d.ts +6 -0
- package/dist/httpSecurity/httpSecurity.config.js +1 -0
- package/dist/httpSecurity/httpSecurity.guard.d.ts +7 -9
- package/dist/httpSecurity/httpSecurity.guard.js +7 -14
- package/dist/httpSecurity/httpSecurity.nodeGuard.d.ts +48 -0
- package/dist/httpSecurity/httpSecurity.nodeGuard.js +32 -0
- package/dist/httpSecurity/httpSecurity.validator.js +20 -9
- package/dist/httpSecurity/index.d.ts +2 -0
- package/dist/httpSecurity/index.js +1 -0
- package/dist/httpServer/core/httpServer.core.d.ts +11 -0
- package/dist/httpServer/core/httpServer.core.js +30 -4
- package/dist/httpTrustProxy/httpTrustProxy.compilation.d.ts +5 -2
- package/dist/httpTrustProxy/httpTrustProxy.compilation.js +19 -2
- package/dist/httpTrustProxy/httpTrustProxy.type.d.ts +3 -1
- package/dist/index.js +0 -1
- package/package.json +5 -5
|
@@ -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, 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 = [];
|
|
@@ -90,7 +90,7 @@ export class HttpRouter {
|
|
|
90
90
|
remove(method, path) {
|
|
91
91
|
const normalizedMethod = normalizeMethod(method);
|
|
92
92
|
const index = this.routes.findIndex((route) => route.definition.method === normalizedMethod &&
|
|
93
|
-
route.definition.path ===
|
|
93
|
+
route.definition.path === normalizeRoutePattern(path));
|
|
94
94
|
if (index === -1) {
|
|
95
95
|
return false;
|
|
96
96
|
}
|
|
@@ -122,11 +122,13 @@ export class HttpRouter {
|
|
|
122
122
|
match(method, path) {
|
|
123
123
|
const normalizedMethod = method.toUpperCase();
|
|
124
124
|
const normalizedPath = normalizePath(path);
|
|
125
|
+
const matchPath = normalizeMatchPath(path);
|
|
125
126
|
const candidates = this.sortedRoutes();
|
|
127
|
+
const allowedForPath = () => collectAllowedMethods(candidates, matchPath, this.routerOptions.caseSensitive);
|
|
126
128
|
const allowed = new Set();
|
|
127
129
|
let pathMatched = false;
|
|
128
130
|
for (const route of candidates) {
|
|
129
|
-
const params = matchCompiledRoute(route,
|
|
131
|
+
const params = matchCompiledRoute(route, matchPath, this.routerOptions.caseSensitive);
|
|
130
132
|
if (!params) {
|
|
131
133
|
continue;
|
|
132
134
|
}
|
|
@@ -137,9 +139,7 @@ export class HttpRouter {
|
|
|
137
139
|
matched: true,
|
|
138
140
|
route: route.definition,
|
|
139
141
|
params,
|
|
140
|
-
allowedMethods: Object.freeze([
|
|
141
|
-
...collectAllowedMethods(candidates, normalizedPath),
|
|
142
|
-
]),
|
|
142
|
+
allowedMethods: Object.freeze([...allowedForPath()]),
|
|
143
143
|
path: normalizedPath,
|
|
144
144
|
method: normalizedMethod,
|
|
145
145
|
};
|
|
@@ -153,16 +153,13 @@ export class HttpRouter {
|
|
|
153
153
|
if (route.definition.method !== "GET") {
|
|
154
154
|
continue;
|
|
155
155
|
}
|
|
156
|
-
const params = matchCompiledRoute(route,
|
|
156
|
+
const params = matchCompiledRoute(route, matchPath, this.routerOptions.caseSensitive);
|
|
157
157
|
if (params) {
|
|
158
158
|
return {
|
|
159
159
|
matched: true,
|
|
160
160
|
route: route.definition,
|
|
161
161
|
params,
|
|
162
|
-
allowedMethods: Object.freeze([
|
|
163
|
-
...collectAllowedMethods(candidates, normalizedPath),
|
|
164
|
-
"HEAD",
|
|
165
|
-
]),
|
|
162
|
+
allowedMethods: Object.freeze([...allowedForPath(), "HEAD"]),
|
|
166
163
|
path: normalizedPath,
|
|
167
164
|
method: normalizedMethod,
|
|
168
165
|
};
|
|
@@ -176,10 +173,7 @@ export class HttpRouter {
|
|
|
176
173
|
matched: true,
|
|
177
174
|
route: undefined,
|
|
178
175
|
params: {},
|
|
179
|
-
allowedMethods: Object.freeze([
|
|
180
|
-
...collectAllowedMethods(candidates, normalizedPath),
|
|
181
|
-
"OPTIONS",
|
|
182
|
-
]),
|
|
176
|
+
allowedMethods: Object.freeze([...allowedForPath(), "OPTIONS"]),
|
|
183
177
|
path: normalizedPath,
|
|
184
178
|
method: normalizedMethod,
|
|
185
179
|
};
|
|
@@ -261,11 +255,11 @@ export class HttpRouter {
|
|
|
261
255
|
/* ------------------------------------------------------------------------ */
|
|
262
256
|
register(method, path, handler, options) {
|
|
263
257
|
const normalizedMethod = normalizeMethod(method);
|
|
264
|
-
const normalizedPath =
|
|
258
|
+
const normalizedPath = normalizeRoutePattern(path);
|
|
265
259
|
if (typeof handler !== "function") {
|
|
266
260
|
throw new HttpRouterError("Route handler must be a function.");
|
|
267
261
|
}
|
|
268
|
-
const compiled = compileRoute(
|
|
262
|
+
const compiled = compileRoute(path, this.routerOptions.strictTrailingSlash ||
|
|
269
263
|
options.strictTrailingSlash === true);
|
|
270
264
|
const existing = this.routes.find((route) => route.definition.method === normalizedMethod &&
|
|
271
265
|
route.definition.path === normalizedPath);
|
|
@@ -290,6 +284,7 @@ export class HttpRouter {
|
|
|
290
284
|
segments: compiled.segments,
|
|
291
285
|
score: compiled.score,
|
|
292
286
|
strictTrailingSlash: compiled.strictTrailingSlash,
|
|
287
|
+
expectsTrailingSlash: compiled.expectsTrailingSlash,
|
|
293
288
|
});
|
|
294
289
|
return () => {
|
|
295
290
|
this.remove(normalizedMethod, normalizedPath);
|
|
@@ -297,9 +292,9 @@ export class HttpRouter {
|
|
|
297
292
|
}
|
|
298
293
|
sortedRoutes() {
|
|
299
294
|
return [...this.routes].sort((left, right) => {
|
|
300
|
-
const
|
|
301
|
-
if (
|
|
302
|
-
return
|
|
295
|
+
const specificity = compareSegmentSpecificity(left.segments, right.segments);
|
|
296
|
+
if (specificity !== 0) {
|
|
297
|
+
return specificity;
|
|
303
298
|
}
|
|
304
299
|
return (extractRouteSequence(left.definition.id) -
|
|
305
300
|
extractRouteSequence(right.definition.id));
|
|
@@ -104,6 +104,12 @@ export interface CompiledRoute {
|
|
|
104
104
|
readonly segments: readonly CompiledSegment[];
|
|
105
105
|
readonly score: number;
|
|
106
106
|
readonly strictTrailingSlash: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Whether the registered pattern ended with a slash.
|
|
109
|
+
*
|
|
110
|
+
* Only consulted when {@link CompiledRoute.strictTrailingSlash} is set.
|
|
111
|
+
*/
|
|
112
|
+
readonly expectsTrailingSlash?: boolean;
|
|
107
113
|
}
|
|
108
114
|
export { HttpRouterError, RouteConflictError, } from "../error/httpRouter.error.js";
|
|
109
115
|
//# sourceMappingURL=httpRouter.type.d.ts.map
|
|
@@ -8,10 +8,71 @@ import type { HttpRequestContext as RequestContext } from "../../../httpRequest/
|
|
|
8
8
|
export declare function getRequestMethod(request: RequestContext): string;
|
|
9
9
|
export declare function getRequestUrl(request: RequestContext): string;
|
|
10
10
|
export declare function getRequestSignal(request: RequestContext): AbortSignal | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Parses a request-target with the canonical parser shared by the request
|
|
13
|
+
* context and path-scoped middleware.
|
|
14
|
+
*/
|
|
11
15
|
export declare function parseUrl(value: string): URL;
|
|
16
|
+
/**
|
|
17
|
+
* Builds the router's query record.
|
|
18
|
+
*
|
|
19
|
+
* Uses the same parser as the Node adapter's `request.query`
|
|
20
|
+
* (`parseQueryString`): a repeated name is an array, the record has a `null`
|
|
21
|
+
* prototype, and `__proto__` / `constructor` / `prototype` are dropped. The
|
|
22
|
+
* two used to disagree (`ctx.query.role` an array, `request.getQuery("role")`
|
|
23
|
+
* the last value), which let middleware and handlers read different values
|
|
24
|
+
* for the same parameter.
|
|
25
|
+
*/
|
|
12
26
|
export declare function parseQuery(params: URLSearchParams): Readonly<Record<string, string | string[]>>;
|
|
27
|
+
/**
|
|
28
|
+
* Normalizes a **request** path.
|
|
29
|
+
*
|
|
30
|
+
* Strips the query string, forces a leading slash, collapses repeated
|
|
31
|
+
* slashes, and trims a trailing slash.
|
|
32
|
+
*
|
|
33
|
+
* This must not be used on a route *pattern*: a pattern may legitimately
|
|
34
|
+
* contain `?` (the optional-parameter marker), which this function treats as
|
|
35
|
+
* the start of a query string. Use {@link normalizeRoutePattern} for
|
|
36
|
+
* patterns.
|
|
37
|
+
*/
|
|
13
38
|
export declare function normalizePath(path: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Normalizes a request path while preserving a single trailing slash.
|
|
41
|
+
*
|
|
42
|
+
* The trailing slash is the only information a strict-trailing-slash route
|
|
43
|
+
* needs and {@link normalizePath} destroys it, so matching runs on this
|
|
44
|
+
* spelling instead.
|
|
45
|
+
*
|
|
46
|
+
* @param path - The raw request path, possibly with a query or fragment.
|
|
47
|
+
* @returns The normalized path, keeping one trailing slash if present.
|
|
48
|
+
*/
|
|
49
|
+
export declare function normalizeMatchPath(path: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Normalizes a route **pattern**.
|
|
52
|
+
*
|
|
53
|
+
* Identical to {@link normalizePath} except that `?` is left alone, so the
|
|
54
|
+
* documented optional-parameter syntax (`/users/:id?`, `/files/{name?}`)
|
|
55
|
+
* survives registration.
|
|
56
|
+
*
|
|
57
|
+
* @param pattern - The raw route pattern.
|
|
58
|
+
* @returns The normalized pattern.
|
|
59
|
+
*/
|
|
60
|
+
export declare function normalizeRoutePattern(pattern: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* Reports whether a path carries a meaningful trailing slash.
|
|
63
|
+
*
|
|
64
|
+
* @param path - The path to inspect.
|
|
65
|
+
* @returns `true` when the path ends with `/` and is not the root path.
|
|
66
|
+
*/
|
|
67
|
+
export declare function hasTrailingSlash(path: string): boolean;
|
|
14
68
|
export declare function splitPath(path: string): string[];
|
|
69
|
+
/**
|
|
70
|
+
* Splits a route pattern into its segments, keeping `?` markers intact.
|
|
71
|
+
*
|
|
72
|
+
* @param pattern - The raw route pattern.
|
|
73
|
+
* @returns The pattern's non-empty segments.
|
|
74
|
+
*/
|
|
75
|
+
export declare function splitRoutePattern(pattern: string): string[];
|
|
15
76
|
export declare function validateParameterName(name: string, path: string): void;
|
|
16
77
|
export declare function decodeRouteValue(value: string): string;
|
|
17
78
|
/**
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Request accessors, URL/query parsing, and path normalization shared by the
|
|
5
5
|
* router core.
|
|
6
6
|
*/
|
|
7
|
+
import { parseRequestTarget } from "../../../httpRequest/target/httpRequest.target.js";
|
|
8
|
+
import { parseQueryString } from "../../../httpQuery/index.js";
|
|
7
9
|
import { InvalidRoutePatternError } from "../error/httpRouter.error.js";
|
|
8
10
|
export function getRequestMethod(request) {
|
|
9
11
|
const value = request.method;
|
|
@@ -19,35 +21,105 @@ export function getRequestUrl(request) {
|
|
|
19
21
|
export function getRequestSignal(request) {
|
|
20
22
|
return request.signal;
|
|
21
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Parses a request-target with the canonical parser shared by the request
|
|
26
|
+
* context and path-scoped middleware.
|
|
27
|
+
*/
|
|
22
28
|
export function parseUrl(value) {
|
|
23
|
-
|
|
24
|
-
return new URL(value, "http://zudojs.local");
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return new URL("/", "http://zudojs.local");
|
|
28
|
-
}
|
|
29
|
+
return parseRequestTarget(value);
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Builds the router's query record.
|
|
33
|
+
*
|
|
34
|
+
* Uses the same parser as the Node adapter's `request.query`
|
|
35
|
+
* (`parseQueryString`): a repeated name is an array, the record has a `null`
|
|
36
|
+
* prototype, and `__proto__` / `constructor` / `prototype` are dropped. The
|
|
37
|
+
* two used to disagree (`ctx.query.role` an array, `request.getQuery("role")`
|
|
38
|
+
* the last value), which let middleware and handlers read different values
|
|
39
|
+
* for the same parameter.
|
|
40
|
+
*/
|
|
30
41
|
export function parseQuery(params) {
|
|
31
|
-
|
|
32
|
-
for (const key of new Set(Array.from(params.keys()))) {
|
|
33
|
-
const values = params.getAll(key);
|
|
34
|
-
result[key] = values.length > 1 ? values : (values[0] ?? "");
|
|
35
|
-
}
|
|
36
|
-
return Object.freeze({
|
|
37
|
-
...result,
|
|
38
|
-
});
|
|
42
|
+
return Object.freeze(parseQueryString(params.toString()));
|
|
39
43
|
}
|
|
40
44
|
/* -------------------------------------------------------------------------- */
|
|
41
45
|
/* Path Helpers */
|
|
42
46
|
/* -------------------------------------------------------------------------- */
|
|
47
|
+
/**
|
|
48
|
+
* Normalizes a **request** path.
|
|
49
|
+
*
|
|
50
|
+
* Strips the query string, forces a leading slash, collapses repeated
|
|
51
|
+
* slashes, and trims a trailing slash.
|
|
52
|
+
*
|
|
53
|
+
* This must not be used on a route *pattern*: a pattern may legitimately
|
|
54
|
+
* contain `?` (the optional-parameter marker), which this function treats as
|
|
55
|
+
* the start of a query string. Use {@link normalizeRoutePattern} for
|
|
56
|
+
* patterns.
|
|
57
|
+
*/
|
|
43
58
|
export function normalizePath(path) {
|
|
44
59
|
if (!path || path === "") {
|
|
45
60
|
return "/";
|
|
46
61
|
}
|
|
47
62
|
const withoutQuery = path.split("?", 1)[0] ?? path;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
return collapsePath(withoutQuery);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Normalizes a request path while preserving a single trailing slash.
|
|
67
|
+
*
|
|
68
|
+
* The trailing slash is the only information a strict-trailing-slash route
|
|
69
|
+
* needs and {@link normalizePath} destroys it, so matching runs on this
|
|
70
|
+
* spelling instead.
|
|
71
|
+
*
|
|
72
|
+
* @param path - The raw request path, possibly with a query or fragment.
|
|
73
|
+
* @returns The normalized path, keeping one trailing slash if present.
|
|
74
|
+
*/
|
|
75
|
+
export function normalizeMatchPath(path) {
|
|
76
|
+
if (!path || path.trim() === "") {
|
|
77
|
+
return "/";
|
|
78
|
+
}
|
|
79
|
+
let target = path.trim();
|
|
80
|
+
if (target.startsWith("http://") || target.startsWith("https://")) {
|
|
81
|
+
try {
|
|
82
|
+
target = new URL(target).pathname;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
/* Keep the original value if it is not a valid absolute URL. */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const withoutQuery = target.split("?", 1)[0] ?? target;
|
|
89
|
+
const withoutHash = withoutQuery.split("#", 1)[0] ?? withoutQuery;
|
|
90
|
+
const collapsed = collapsePath(withoutHash);
|
|
91
|
+
return hasTrailingSlash(withoutHash) && collapsed !== "/"
|
|
92
|
+
? `${collapsed}/`
|
|
93
|
+
: collapsed;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Normalizes a route **pattern**.
|
|
97
|
+
*
|
|
98
|
+
* Identical to {@link normalizePath} except that `?` is left alone, so the
|
|
99
|
+
* documented optional-parameter syntax (`/users/:id?`, `/files/{name?}`)
|
|
100
|
+
* survives registration.
|
|
101
|
+
*
|
|
102
|
+
* @param pattern - The raw route pattern.
|
|
103
|
+
* @returns The normalized pattern.
|
|
104
|
+
*/
|
|
105
|
+
export function normalizeRoutePattern(pattern) {
|
|
106
|
+
if (!pattern || pattern === "") {
|
|
107
|
+
return "/";
|
|
108
|
+
}
|
|
109
|
+
return collapsePath(pattern);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Reports whether a path carries a meaningful trailing slash.
|
|
113
|
+
*
|
|
114
|
+
* @param path - The path to inspect.
|
|
115
|
+
* @returns `true` when the path ends with `/` and is not the root path.
|
|
116
|
+
*/
|
|
117
|
+
export function hasTrailingSlash(path) {
|
|
118
|
+
const trimmed = path.replace(/\/{2,}/g, "/");
|
|
119
|
+
return trimmed.length > 1 && trimmed.endsWith("/");
|
|
120
|
+
}
|
|
121
|
+
function collapsePath(value) {
|
|
122
|
+
let normalized = value.startsWith("/") ? value : `/${value}`;
|
|
51
123
|
normalized = normalized.replace(/\/{2,}/g, "/");
|
|
52
124
|
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
53
125
|
normalized = normalized.slice(0, -1);
|
|
@@ -61,6 +133,19 @@ export function splitPath(path) {
|
|
|
61
133
|
}
|
|
62
134
|
return normalized.split("/").filter(Boolean);
|
|
63
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Splits a route pattern into its segments, keeping `?` markers intact.
|
|
138
|
+
*
|
|
139
|
+
* @param pattern - The raw route pattern.
|
|
140
|
+
* @returns The pattern's non-empty segments.
|
|
141
|
+
*/
|
|
142
|
+
export function splitRoutePattern(pattern) {
|
|
143
|
+
const normalized = normalizeRoutePattern(pattern);
|
|
144
|
+
if (normalized === "/") {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
return normalized.split("/").filter(Boolean);
|
|
148
|
+
}
|
|
64
149
|
export function validateParameterName(name, path) {
|
|
65
150
|
if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(name)) {
|
|
66
151
|
throw new InvalidRoutePatternError(path, `Invalid parameter name "${name}".`);
|
|
@@ -24,6 +24,17 @@ export interface RouteDispatchContext {
|
|
|
24
24
|
export interface RouteDispatchOptions {
|
|
25
25
|
readonly onError?: RouteDispatchErrorHandler;
|
|
26
26
|
readonly onComplete?: RouteDispatchCompleteHandler;
|
|
27
|
+
/**
|
|
28
|
+
* Keeps the response the middleware chain produced.
|
|
29
|
+
*
|
|
30
|
+
* By default a response returned by a route handler is merged into the
|
|
31
|
+
* response passed to `dispatch()`, overwriting its status, headers, cookies
|
|
32
|
+
* and body. With this set, the handler's response is left alone and the
|
|
33
|
+
* dispatch response is returned exactly as the middleware chain built it.
|
|
34
|
+
*
|
|
35
|
+
* The option was previously declared and never read, so it silently did
|
|
36
|
+
* nothing.
|
|
37
|
+
*/
|
|
27
38
|
readonly preserveResponse?: boolean;
|
|
28
39
|
}
|
|
29
40
|
export type RouteDispatchErrorHandler = (error: unknown, context: RouteDispatchContext) => void | Promise<void>;
|
|
@@ -104,7 +104,7 @@ export class RouteDispatcher {
|
|
|
104
104
|
async execute(context) {
|
|
105
105
|
const state = new RouterMiddlewareState();
|
|
106
106
|
const middleware = context.route.middleware.map((layer) => toRouteMiddleware(layer, context.route, state));
|
|
107
|
-
const handler = toDispatchHandler(context.route.handler, context);
|
|
107
|
+
const handler = toDispatchHandler(context.route.handler, context, this.options.preserveResponse === true);
|
|
108
108
|
let index = -1;
|
|
109
109
|
const dispatchNext = async (current) => {
|
|
110
110
|
if (current <= index) {
|
|
@@ -220,7 +220,7 @@ function normalizeHandler(handler) {
|
|
|
220
220
|
* A response context returned by the handler is merged into the dispatch
|
|
221
221
|
* response so the dispatcher's response object stays authoritative.
|
|
222
222
|
*/
|
|
223
|
-
function toDispatchHandler(handler, context) {
|
|
223
|
+
function toDispatchHandler(handler, context, preserveResponse = false) {
|
|
224
224
|
return async (request, response) => {
|
|
225
225
|
const result = await handler(createRouterContext({
|
|
226
226
|
request,
|
|
@@ -228,7 +228,9 @@ function toDispatchHandler(handler, context) {
|
|
|
228
228
|
params: context.params,
|
|
229
229
|
signal: getRequestSignal(request),
|
|
230
230
|
}));
|
|
231
|
-
if (
|
|
231
|
+
if (!preserveResponse &&
|
|
232
|
+
result instanceof HttpResponseContext &&
|
|
233
|
+
result !== response) {
|
|
232
234
|
/*
|
|
233
235
|
* Everything the handler put on its response must survive the merge.
|
|
234
236
|
* Copying only status, headers and body dropped every cookie the
|
|
@@ -5,14 +5,28 @@
|
|
|
5
5
|
* case sensitivity, optional parameters, per-parameter regular expression
|
|
6
6
|
* constraints, and trailing wildcards.
|
|
7
7
|
*/
|
|
8
|
-
import { decodeRouteSegment, normalizePath, splitPath, } from "../core/util/httpRoute.util.js";
|
|
8
|
+
import { decodeRouteSegment, hasTrailingSlash, normalizePath, splitPath, } from "../core/util/httpRoute.util.js";
|
|
9
9
|
export function matchCompiledRoute(route, path, caseSensitive) {
|
|
10
|
+
/*
|
|
11
|
+
* `strictTrailingSlash` used to be compiled and stored but never read, so a
|
|
12
|
+
* strict router still answered `/users/` with the `/users` route. The
|
|
13
|
+
* request path reaches this function with its trailing slash intact
|
|
14
|
+
* (`normalizeMatchPath`), so the two spellings can finally be told apart.
|
|
15
|
+
*/
|
|
16
|
+
if (route.strictTrailingSlash &&
|
|
17
|
+
hasTrailingSlash(path) !== (route.expectsTrailingSlash ?? false)) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
10
20
|
const routePath = normalizePath(path);
|
|
11
21
|
const inputSegments = splitPath(routePath);
|
|
12
22
|
const output = {};
|
|
13
23
|
const routeSegments = route.segments;
|
|
14
24
|
let inputIndex = 0;
|
|
15
|
-
for (
|
|
25
|
+
for (let segmentIndex = 0; segmentIndex < routeSegments.length; segmentIndex += 1) {
|
|
26
|
+
const segment = routeSegments[segmentIndex];
|
|
27
|
+
if (segment === undefined) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
16
30
|
if (segment.type === "wildcard") {
|
|
17
31
|
/*
|
|
18
32
|
* Decode each segment separately and re-join. Decoding the joined tail
|
|
@@ -33,10 +47,18 @@ export function matchCompiledRoute(route, path, caseSensitive) {
|
|
|
33
47
|
break;
|
|
34
48
|
}
|
|
35
49
|
const input = inputSegments[inputIndex];
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
if (segment.type === "parameter" && segment.optional) {
|
|
51
|
+
/*
|
|
52
|
+
* An optional parameter only claims a segment when the segments that
|
|
53
|
+
* follow it still have enough input left. Without that check
|
|
54
|
+
* `/account/:id?/profile` swallowed `profile` as the id and then found
|
|
55
|
+
* nothing to match its literal tail.
|
|
56
|
+
*/
|
|
57
|
+
const remainingInput = inputSegments.length - inputIndex;
|
|
58
|
+
if (input === undefined ||
|
|
59
|
+
remainingInput <= requiredSegments(routeSegments, segmentIndex + 1)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
40
62
|
}
|
|
41
63
|
if (input === undefined) {
|
|
42
64
|
return undefined;
|
|
@@ -69,4 +91,27 @@ export function matchCompiledRoute(route, path, caseSensitive) {
|
|
|
69
91
|
}
|
|
70
92
|
return Object.freeze(output);
|
|
71
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Counts the segments from `start` onwards that must consume input.
|
|
96
|
+
*
|
|
97
|
+
* Optional parameters and wildcards can match nothing, so they do not count.
|
|
98
|
+
*
|
|
99
|
+
* @param segments - The route's compiled segments.
|
|
100
|
+
* @param start - The index to count from.
|
|
101
|
+
* @returns The number of segments that require an input segment.
|
|
102
|
+
*/
|
|
103
|
+
function requiredSegments(segments, start) {
|
|
104
|
+
let required = 0;
|
|
105
|
+
for (let index = start; index < segments.length; index += 1) {
|
|
106
|
+
const segment = segments[index];
|
|
107
|
+
if (segment === undefined || segment.type === "wildcard") {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (segment.type === "parameter" && segment.optional) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
required += 1;
|
|
114
|
+
}
|
|
115
|
+
return required;
|
|
116
|
+
}
|
|
72
117
|
//# sourceMappingURL=httpRoute.matcher.core.js.map
|
|
@@ -51,7 +51,6 @@ export declare class RouteMatcher {
|
|
|
51
51
|
stats(): RouteMatcherStats;
|
|
52
52
|
resetStats(): void;
|
|
53
53
|
private collect;
|
|
54
|
-
private matchFirstAllowed;
|
|
55
54
|
}
|
|
56
55
|
export declare function createRouteMatcher(router: HttpRouter, options?: RouteMatcherOptions): RouteMatcher;
|
|
57
56
|
export declare function matchRoute(matcher: RouteMatcher, method: string, path: string): RouteMatcherResult | undefined;
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* pathname. Route registration lives in the router core; per-route segment
|
|
6
6
|
* matching lives in `httpRoute.matcher.core.ts`.
|
|
7
7
|
*/
|
|
8
|
-
import { normalizeMethod } from "../core/factory/httpRoute.factory.base.js";
|
|
8
|
+
import { createOptionsResponse, normalizeMethod, } from "../core/factory/httpRoute.factory.base.js";
|
|
9
|
+
import { normalizeMatchPath } from "../core/util/httpRoute.util.js";
|
|
9
10
|
import { formatAllowHeader } from "../../httpMethods/http.methods.js";
|
|
10
11
|
import { matchCompiledRoute } from "./httpRoute.matcher.core.js";
|
|
11
12
|
/* -------------------------------------------------------------------------- */
|
|
@@ -31,14 +32,20 @@ export class RouteMatcher {
|
|
|
31
32
|
match(request) {
|
|
32
33
|
this.requests += 1;
|
|
33
34
|
const method = normalizeMethod(request.method);
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
/*
|
|
36
|
+
* The raw target is threaded through so a strict-trailing-slash route can
|
|
37
|
+
* still tell `/users` from `/users/`; only the reported path is
|
|
38
|
+
* normalized.
|
|
39
|
+
*/
|
|
40
|
+
const target = request.path;
|
|
41
|
+
const path = normalizeRequestPath(target);
|
|
42
|
+
const direct = this.matchMethod(method, target);
|
|
36
43
|
if (direct) {
|
|
37
44
|
this.matches += 1;
|
|
38
45
|
return direct;
|
|
39
46
|
}
|
|
40
47
|
if (method === "HEAD" && this.matcherOptions.allowHeadFallback) {
|
|
41
|
-
const fallback = this.matchMethod("GET",
|
|
48
|
+
const fallback = this.matchMethod("GET", target);
|
|
42
49
|
if (fallback) {
|
|
43
50
|
this.matches += 1;
|
|
44
51
|
return {
|
|
@@ -48,13 +55,25 @@ export class RouteMatcher {
|
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
57
|
if (method === "OPTIONS" && this.matcherOptions.allowOptionsFallback) {
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
58
|
+
const allowed = this.allowedMethods(target);
|
|
59
|
+
if (allowed.allowed) {
|
|
53
60
|
this.matches += 1;
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
/*
|
|
62
|
+
* The fallback must never carry an executable route. It used to
|
|
63
|
+
* answer with the first route registered on the path under ANY
|
|
64
|
+
* method, so `OPTIONS /accounts/42` ran the `DELETE` handler — past
|
|
65
|
+
* any CSRF guard that treats `OPTIONS` as safe. The synthetic route
|
|
66
|
+
* below owns no middleware and answers `204` with `Allow`, matching
|
|
67
|
+
* what `HttpRouter.match()` already does.
|
|
68
|
+
*/
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
route: createOptionsRoute(path, allowed),
|
|
71
|
+
params: Object.freeze({}),
|
|
72
|
+
method,
|
|
73
|
+
path,
|
|
74
|
+
score: 0,
|
|
56
75
|
matchedBy: "options-fallback",
|
|
57
|
-
};
|
|
76
|
+
});
|
|
58
77
|
}
|
|
59
78
|
}
|
|
60
79
|
this.misses += 1;
|
|
@@ -63,11 +82,12 @@ export class RouteMatcher {
|
|
|
63
82
|
matchMethod(method, path) {
|
|
64
83
|
const normalizedMethod = normalizeMethod(method);
|
|
65
84
|
const normalizedPath = normalizeRequestPath(path);
|
|
85
|
+
const matchPath = normalizeMatchPath(path);
|
|
66
86
|
for (const route of this.router.compiled()) {
|
|
67
87
|
if (!methodApplies(route.definition.method, normalizedMethod)) {
|
|
68
88
|
continue;
|
|
69
89
|
}
|
|
70
|
-
const params = matchCompiledRoute(route,
|
|
90
|
+
const params = matchCompiledRoute(route, matchPath, this.matcherOptions.caseSensitive);
|
|
71
91
|
if (!params) {
|
|
72
92
|
continue;
|
|
73
93
|
}
|
|
@@ -80,23 +100,22 @@ export class RouteMatcher {
|
|
|
80
100
|
/* ------------------------------------------------------------------------ */
|
|
81
101
|
matchAll(request) {
|
|
82
102
|
const method = normalizeMethod(request.method);
|
|
83
|
-
|
|
84
|
-
return this.collect(method, path);
|
|
103
|
+
return this.collect(method, request.path);
|
|
85
104
|
}
|
|
86
105
|
/* ------------------------------------------------------------------------ */
|
|
87
106
|
/* Path Matching */
|
|
88
107
|
/* ------------------------------------------------------------------------ */
|
|
89
108
|
matchPath(path) {
|
|
90
|
-
return this.collect("*",
|
|
109
|
+
return this.collect("*", path);
|
|
91
110
|
}
|
|
92
111
|
/* ------------------------------------------------------------------------ */
|
|
93
112
|
/* Allowed Methods */
|
|
94
113
|
/* ------------------------------------------------------------------------ */
|
|
95
114
|
allowedMethods(path) {
|
|
96
|
-
const
|
|
115
|
+
const matchPath = normalizeMatchPath(path);
|
|
97
116
|
const methods = new Set();
|
|
98
117
|
for (const route of this.router.compiled()) {
|
|
99
|
-
const params = matchCompiledRoute(route,
|
|
118
|
+
const params = matchCompiledRoute(route, matchPath, this.matcherOptions.caseSensitive);
|
|
100
119
|
if (params) {
|
|
101
120
|
methods.add(route.definition.method);
|
|
102
121
|
}
|
|
@@ -140,29 +159,22 @@ export class RouteMatcher {
|
|
|
140
159
|
/* ------------------------------------------------------------------------ */
|
|
141
160
|
collect(method, path) {
|
|
142
161
|
const results = [];
|
|
162
|
+
const normalizedPath = normalizeRequestPath(path);
|
|
163
|
+
const matchPath = normalizeMatchPath(path);
|
|
143
164
|
const seen = new Set();
|
|
144
165
|
for (const route of this.router.compiled()) {
|
|
145
166
|
if (!methodApplies(route.definition.method, method)) {
|
|
146
167
|
continue;
|
|
147
168
|
}
|
|
148
|
-
const params = matchCompiledRoute(route,
|
|
169
|
+
const params = matchCompiledRoute(route, matchPath, this.matcherOptions.caseSensitive);
|
|
149
170
|
if (!params || seen.has(route.definition.id)) {
|
|
150
171
|
continue;
|
|
151
172
|
}
|
|
152
173
|
seen.add(route.definition.id);
|
|
153
|
-
results.push(createResult(route, params, method,
|
|
174
|
+
results.push(createResult(route, params, method, normalizedPath));
|
|
154
175
|
}
|
|
155
176
|
return Object.freeze(results);
|
|
156
177
|
}
|
|
157
|
-
matchFirstAllowed(methods, path) {
|
|
158
|
-
for (const method of methods) {
|
|
159
|
-
const result = this.matchMethod(method, path);
|
|
160
|
-
if (result) {
|
|
161
|
-
return result;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return undefined;
|
|
165
|
-
}
|
|
166
178
|
}
|
|
167
179
|
/* -------------------------------------------------------------------------- */
|
|
168
180
|
/* Factory */
|
|
@@ -200,6 +212,32 @@ function createResult(route, params, method, path) {
|
|
|
200
212
|
matchedBy: route.definition.method === "*" ? "wildcard" : "exact",
|
|
201
213
|
});
|
|
202
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Builds the synthetic route an `OPTIONS` fallback resolves to.
|
|
217
|
+
*
|
|
218
|
+
* It carries no middleware and a handler that only answers `204` with the
|
|
219
|
+
* path's `Allow` header, so an `OPTIONS` request can never reach a handler
|
|
220
|
+
* registered for another method.
|
|
221
|
+
*
|
|
222
|
+
* @param path - The normalized request path.
|
|
223
|
+
* @param allowed - The methods registered on that path.
|
|
224
|
+
* @returns The synthetic route definition.
|
|
225
|
+
*/
|
|
226
|
+
function createOptionsRoute(path, allowed) {
|
|
227
|
+
const methods = allowed.methods.includes("OPTIONS")
|
|
228
|
+
? allowed.methods
|
|
229
|
+
: [...allowed.methods, "OPTIONS"];
|
|
230
|
+
return Object.freeze({
|
|
231
|
+
id: "route:options-fallback",
|
|
232
|
+
method: "OPTIONS",
|
|
233
|
+
path,
|
|
234
|
+
name: undefined,
|
|
235
|
+
params: Object.freeze({}),
|
|
236
|
+
metadata: Object.freeze({}),
|
|
237
|
+
handler: () => createOptionsResponse(methods),
|
|
238
|
+
middleware: Object.freeze([]),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
203
241
|
/* -------------------------------------------------------------------------- */
|
|
204
242
|
/* Method Utilities */
|
|
205
243
|
/* -------------------------------------------------------------------------- */
|