@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.
Files changed (127) hide show
  1. package/README.md +221 -0
  2. package/dist/httpAdapter/http.adapters.d.ts +24 -3
  3. package/dist/httpAdapter/http.adapters.js +20 -11
  4. package/dist/httpAdapter/node/httpNode.adapter.d.ts +2 -1
  5. package/dist/httpAdapter/node/httpNode.adapter.js +17 -2
  6. package/dist/httpAdapter/node/httpNode.request.js +7 -1
  7. package/dist/httpAdapter/node/httpNode.type.d.ts +14 -0
  8. package/dist/httpAgent/http.agent.d.ts +22 -3
  9. package/dist/httpAgent/http.agent.js +52 -14
  10. package/dist/httpCacheControl/httpCacheControl.freshness.d.ts +7 -1
  11. package/dist/httpCacheControl/httpCacheControl.freshness.js +30 -3
  12. package/dist/httpClient/httpClient.retry.d.ts +17 -12
  13. package/dist/httpClient/httpClient.retry.js +35 -10
  14. package/dist/httpClient/httpClient.type.d.ts +14 -0
  15. package/dist/httpErrors/httpError.base.js +2 -2
  16. package/dist/httpErrors/httpError.util.d.ts +8 -0
  17. package/dist/httpErrors/httpError.util.js +12 -0
  18. package/dist/httpFetchMount/httpFetchMount.core.d.ts +25 -0
  19. package/dist/httpFetchMount/httpFetchMount.core.js +84 -0
  20. package/dist/httpFetchMount/httpFetchMount.request.d.ts +21 -0
  21. package/dist/httpFetchMount/httpFetchMount.request.js +100 -0
  22. package/dist/httpFetchMount/httpFetchMount.type.d.ts +56 -0
  23. package/dist/httpFetchMount/httpFetchMount.type.js +5 -0
  24. package/dist/httpFetchMount/index.d.ts +11 -0
  25. package/dist/httpFetchMount/index.js +10 -0
  26. package/dist/httpKeepAlive/httpKeepAlive.core.js +14 -2
  27. package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.d.ts +18 -0
  28. package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.js +17 -1
  29. package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.d.ts +6 -3
  30. package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.js +34 -6
  31. package/dist/httpMiddleware/builtin/security/httpMiddleware.security.d.ts +11 -0
  32. package/dist/httpMiddleware/builtin/security/httpMiddleware.security.js +25 -12
  33. package/dist/httpMiddleware/httpMiddleware.type.d.ts +9 -1
  34. package/dist/httpMiddleware/pipeline/httpPipeline.execution.js +22 -45
  35. package/dist/httpMiddleware/pipeline/httpPipeline.guardResponse.d.ts +36 -0
  36. package/dist/httpMiddleware/pipeline/httpPipeline.guardResponse.js +57 -0
  37. package/dist/httpMiddleware/pipeline/httpPipeline.helper.d.ts +2 -1
  38. package/dist/httpMiddleware/pipeline/httpPipeline.helper.js +10 -0
  39. package/dist/httpMiddleware/pipeline/index.d.ts +1 -0
  40. package/dist/httpMiddleware/pipeline/index.js +1 -0
  41. package/dist/httpNegotiation/httpNegotiation.core.d.ts +13 -0
  42. package/dist/httpNegotiation/httpNegotiation.core.js +60 -8
  43. package/dist/httpOpenApi/httpOpenApi.document.d.ts +44 -0
  44. package/dist/httpOpenApi/httpOpenApi.document.js +61 -0
  45. package/dist/httpOpenApi/httpOpenApi.mount.d.ts +31 -0
  46. package/dist/httpOpenApi/httpOpenApi.mount.js +58 -0
  47. package/dist/httpOpenApi/httpOpenApi.type.d.ts +54 -0
  48. package/dist/httpOpenApi/httpOpenApi.type.js +5 -0
  49. package/dist/httpOpenApi/index.d.ts +13 -0
  50. package/dist/httpOpenApi/index.js +12 -0
  51. package/dist/httpOpenApi/routeTable/index.d.ts +11 -0
  52. package/dist/httpOpenApi/routeTable/index.js +11 -0
  53. package/dist/httpOpenApi/routeTable/routeTable.collect.d.ts +19 -0
  54. package/dist/httpOpenApi/routeTable/routeTable.collect.js +89 -0
  55. package/dist/httpOpenApi/routeTable/routeTable.merge.d.ts +15 -0
  56. package/dist/httpOpenApi/routeTable/routeTable.merge.js +37 -0
  57. package/dist/httpOpenApi/routeTable/routeTable.template.d.ts +30 -0
  58. package/dist/httpOpenApi/routeTable/routeTable.template.js +67 -0
  59. package/dist/httpProxy/http.proxy.d.ts +9 -0
  60. package/dist/httpProxy/http.proxy.js +35 -3
  61. package/dist/httpQuery/index.d.ts +11 -2
  62. package/dist/httpQuery/index.js +11 -2
  63. package/dist/httpQuery/queryParse/index.d.ts +10 -0
  64. package/dist/httpQuery/queryParse/index.js +10 -0
  65. package/dist/httpQuery/queryParse/queryParse.flat.d.ts +13 -0
  66. package/dist/httpQuery/queryParse/queryParse.flat.js +44 -0
  67. package/dist/httpQuery/queryParse/queryParse.nested.d.ts +25 -0
  68. package/dist/httpQuery/queryParse/queryParse.nested.js +112 -0
  69. package/dist/httpQuery/queryParse/queryParse.tokenizer.d.ts +37 -0
  70. package/dist/httpQuery/queryParse/queryParse.tokenizer.js +95 -0
  71. package/dist/httpQuery/queryRequest/index.d.ts +9 -0
  72. package/dist/httpQuery/queryRequest/index.js +9 -0
  73. package/dist/httpQuery/queryRequest/query.request.d.ts +43 -0
  74. package/dist/httpQuery/queryRequest/query.request.js +96 -0
  75. package/dist/httpQuery/querySerialize/index.d.ts +10 -0
  76. package/dist/httpQuery/querySerialize/index.js +10 -0
  77. package/dist/httpQuery/querySerialize/query.util.d.ts +21 -0
  78. package/dist/httpQuery/querySerialize/query.util.js +67 -0
  79. package/dist/httpQuery/querySerialize/querySerialize.core.d.ts +12 -0
  80. package/dist/httpQuery/querySerialize/querySerialize.core.js +97 -0
  81. package/dist/httpQuery/queryTypes/index.d.ts +11 -0
  82. package/dist/httpQuery/queryTypes/index.js +9 -0
  83. package/dist/httpQuery/queryTypes/query.container.d.ts +16 -0
  84. package/dist/httpQuery/queryTypes/query.container.js +51 -0
  85. package/dist/httpQuery/queryTypes/query.limit.d.ts +25 -0
  86. package/dist/httpQuery/queryTypes/query.limit.js +32 -0
  87. package/dist/httpQuery/queryTypes/query.type.d.ts +62 -0
  88. package/dist/httpQuery/queryTypes/query.type.js +2 -0
  89. package/dist/httpRedirect/http.redirect.d.ts +6 -0
  90. package/dist/httpRedirect/http.redirect.js +53 -2
  91. package/dist/httpRequest/http.request.d.ts +61 -2
  92. package/dist/httpRequest/http.request.js +86 -35
  93. package/dist/httpRequest/httpRequest.context.d.ts +8 -0
  94. package/dist/httpRequest/httpRequest.context.js +23 -18
  95. package/dist/httpRequest/index.d.ts +1 -0
  96. package/dist/httpRequest/index.js +1 -0
  97. package/dist/httpRequest/requestId/httpRequest.requestId.d.ts +25 -0
  98. package/dist/httpRequest/requestId/httpRequest.requestId.js +34 -0
  99. package/dist/httpRequest/requestId/index.d.ts +7 -0
  100. package/dist/httpRequest/requestId/index.js +7 -0
  101. package/dist/httpRequest/target/httpRequest.target.d.ts +2 -2
  102. package/dist/httpRequest/target/httpRequest.target.js +23 -5
  103. package/dist/httpResponse/httpResponse.writer.js +15 -0
  104. package/dist/httpRouter/core/factory/httpRoute.factory.base.d.ts +24 -4
  105. package/dist/httpRouter/core/factory/httpRoute.factory.base.js +73 -17
  106. package/dist/httpRouter/core/factory/httpRoute.factory.js +3 -3
  107. package/dist/httpRouter/core/group/httpRouterGroup.core.js +13 -0
  108. package/dist/httpRouter/core/register/httpRouter.register.js +19 -21
  109. package/dist/httpRouter/core/types/httpRouter.type.d.ts +37 -1
  110. package/dist/httpRouter/core/util/httpRoute.util.d.ts +55 -0
  111. package/dist/httpRouter/core/util/httpRoute.util.js +101 -4
  112. package/dist/httpRouter/dispatch/httpRoute.dispatcher.d.ts +11 -0
  113. package/dist/httpRouter/dispatch/httpRoute.dispatcher.js +25 -6
  114. package/dist/httpRouter/matching/httpRoute.matcher.core.js +51 -6
  115. package/dist/httpRouter/matching/httpRoute.matcher.d.ts +0 -1
  116. package/dist/httpRouter/matching/httpRoute.matcher.js +64 -26
  117. package/dist/httpRouter/pattern/httpRoute.pattern.parse.d.ts +16 -0
  118. package/dist/httpRouter/pattern/httpRoute.pattern.parse.js +40 -11
  119. package/dist/httpRouter/pattern/index.d.ts +1 -1
  120. package/dist/httpRouter/pattern/index.js +1 -1
  121. package/dist/httpSecurity/httpSecurity.config.js +4 -1
  122. package/dist/httpSecurity/httpSecurity.validator.js +16 -7
  123. package/dist/httpServer/factory/httpServer.factory.d.ts +10 -9
  124. package/dist/httpServer/factory/httpServer.factory.js +8 -0
  125. package/dist/index.d.ts +2 -0
  126. package/dist/index.js +2 -0
  127. package/package.json +11 -8
@@ -12,24 +12,37 @@ import { parseCacheControl } from "./core/httpCacheControl.parse.js";
12
12
  * the origin marked must-revalidate-before-reuse must never be reported
13
13
  * fresh.
14
14
  *
15
+ * The response's current age follows RFC 9111 section 4.2.3: the `Age`
16
+ * header plus the time elapsed since the response's `Date`. Reading `Age`
17
+ * alone — as this used to — left every response without that header aged
18
+ * `0` forever, so `isFresh()` answered `true` for a response of any age.
19
+ *
15
20
  * @param responseHeaders - The cached response's headers.
16
21
  * @param responseDate - The response's `Date`, if already parsed.
22
+ * @param now - The current time, defaulting to `Date.now()`.
17
23
  * @returns The freshness calculation.
18
24
  */
19
- export function calculateFreshness(responseHeaders, responseDate) {
25
+ export function calculateFreshness(responseHeaders, responseDate, now = new Date()) {
20
26
  const cacheControl = responseHeaders["cache-control"];
21
27
  const directives = parseCacheControl(cacheControl);
22
- const date = responseDate ?? new Date(responseHeaders["date"] ?? Date.now());
28
+ const date = responseDate ?? parseDateHeader(responseHeaders["date"], now);
23
29
  /*
24
30
  * RFC 9111 section 5.1: Age is a non-negative delta-seconds. A negative or
25
31
  * suffixed value would otherwise inflate freshness without bound, which
26
32
  * pins a poisoned response in cache far past the origin's TTL.
27
33
  */
28
34
  const rawAge = (responseHeaders["age"] ?? "").trim();
29
- const age = /^\d+$/.test(rawAge) ? Number(rawAge) : 0;
35
+ const ageHeader = /^\d+$/.test(rawAge) ? Number(rawAge) : 0;
36
+ const residentSeconds = Math.max(0, Math.floor((now.getTime() - date.getTime()) / 1_000));
37
+ const age = ageHeader + residentSeconds;
30
38
  const expiresHeader = responseHeaders["expires"];
31
39
  const expires = expiresHeader ? parseExpires(expiresHeader) : undefined;
32
40
  const effectiveMaxAge = resolveMaxAge(directives, expires, date);
41
+ /*
42
+ * With an `Expires`-derived lifetime this reduces to `expires - now`,
43
+ * because the lifetime is measured from `Date` and the age is measured to
44
+ * `now`.
45
+ */
33
46
  const remaining = Math.max(0, effectiveMaxAge - age);
34
47
  const stale = remaining <= 0 ||
35
48
  directives.noCache === true ||
@@ -43,6 +56,20 @@ export function calculateFreshness(responseHeaders, responseDate) {
43
56
  remaining,
44
57
  };
45
58
  }
59
+ /**
60
+ * Parses a `Date` header, falling back to the current time.
61
+ *
62
+ * @param value - The raw `Date` value.
63
+ * @param now - The fallback instant.
64
+ * @returns The parsed date.
65
+ */
66
+ function parseDateHeader(value, now) {
67
+ if (value === undefined) {
68
+ return now;
69
+ }
70
+ const parsed = new Date(value);
71
+ return Number.isNaN(parsed.getTime()) ? now : parsed;
72
+ }
46
73
  /**
47
74
  * Parses an `Expires` header, treating an unparseable value as expired.
48
75
  *
@@ -4,17 +4,8 @@
4
4
  * Handles retry configuration, status-based retry decisions,
5
5
  * exponential backoff, and delay utilities.
6
6
  */
7
- import type { HttpClientMethod } from "./httpClient.type.js";
8
- /** Retry options for the HTTP client. */
9
- export interface HttpRetryOptions {
10
- readonly retries?: number;
11
- readonly retryDelay?: number;
12
- readonly maxRetryDelay?: number;
13
- readonly retryStatusCodes?: readonly number[];
14
- readonly retryMethods?: readonly HttpClientMethod[];
15
- readonly retryOnNetworkError?: boolean;
16
- readonly backoff?: "fixed" | "exponential";
17
- }
7
+ import type { HttpRetryOptions } from "./httpClient.type.js";
8
+ export type { HttpRetryOptions } from "./httpClient.type.js";
18
9
  /**
19
10
  * Normalize retry options with defaults.
20
11
  */
@@ -25,10 +16,24 @@ export declare function normalizeRetryOptions(options?: HttpRetryOptions): HttpR
25
16
  export declare function shouldRetryStatus(status: number, method: string, retry?: HttpRetryOptions): boolean;
26
17
  /**
27
18
  * Check if an error should trigger a retry.
19
+ *
20
+ * Transport failures are retried when `retryOnNetworkError` is on, and
21
+ * timeouts when `retryOnTimeout` is on (it defaults to
22
+ * `retryOnNetworkError`). Both apply only to `retryMethods`, which default
23
+ * to the idempotent `GET`, `HEAD` and `OPTIONS`. A caller's own abort is
24
+ * never retried.
28
25
  */
29
26
  export declare function shouldRetryError(error: unknown, method: string, retry?: HttpRetryOptions): boolean;
30
27
  /**
31
- * Calculate retry delay with exponential backoff.
28
+ * Calculate the wait before retry number `attempt + 1`.
29
+ *
30
+ * The delay is `retryDelay` (fixed backoff) or `retryDelay * 2^attempt`
31
+ * (exponential), capped at `maxRetryDelay`. With `jitter` (the default) the
32
+ * wait is drawn uniformly from 0 up to that delay ("full jitter"), so a
33
+ * burst of clients that failed together does not retry in lockstep;
34
+ * `jitter: false` waits exactly the delay. Jitter used to add up to a
35
+ * fixed 1000 ms whatever `retryDelay` was, so `retryDelay: 50` could wait
36
+ * a second.
32
37
  */
33
38
  export declare function calculateRetryDelay(attempt: number, retry: HttpRetryOptions): number;
34
39
  /**
@@ -4,6 +4,7 @@
4
4
  * Handles retry configuration, status-based retry decisions,
5
5
  * exponential backoff, and delay utilities.
6
6
  */
7
+ import { HttpClientTimeoutError } from "./httpClient.error.js";
7
8
  /**
8
9
  * Normalize retry options with defaults.
9
10
  */
@@ -17,7 +18,9 @@ export function normalizeRetryOptions(options) {
17
18
  retryStatusCodes: options.retryStatusCodes ?? [429, 502, 503, 504],
18
19
  retryMethods: options.retryMethods ?? ["GET", "HEAD", "OPTIONS"],
19
20
  retryOnNetworkError: options.retryOnNetworkError ?? true,
21
+ retryOnTimeout: options.retryOnTimeout ?? options.retryOnNetworkError ?? true,
20
22
  backoff: options.backoff ?? "exponential",
23
+ jitter: options.jitter ?? true,
21
24
  };
22
25
  }
23
26
  /**
@@ -32,20 +35,35 @@ export function shouldRetryStatus(status, method, retry) {
32
35
  }
33
36
  /**
34
37
  * Check if an error should trigger a retry.
38
+ *
39
+ * Transport failures are retried when `retryOnNetworkError` is on, and
40
+ * timeouts when `retryOnTimeout` is on (it defaults to
41
+ * `retryOnNetworkError`). Both apply only to `retryMethods`, which default
42
+ * to the idempotent `GET`, `HEAD` and `OPTIONS`. A caller's own abort is
43
+ * never retried.
35
44
  */
36
45
  export function shouldRetryError(error, method, retry) {
37
46
  if (!retry?.retries)
38
47
  return false;
39
- if (!retry.retryOnNetworkError)
40
- return false;
41
48
  /*
42
- * A connection that drops after the server processed the request is
43
- * indistinguishable from one that never arrived, so replaying a
44
- * non-idempotent method duplicates its side effects. `retryMethods` exists
45
- * for exactly this and was honoured only on the status path.
49
+ * A connection that drops (or a request that times out) after the server
50
+ * processed it is indistinguishable from one that never arrived, so
51
+ * replaying a non-idempotent method duplicates its side effects.
52
+ * `retryMethods` exists for exactly this and was honoured only on the
53
+ * status path.
46
54
  */
47
55
  if (!retry.retryMethods?.includes(method))
48
56
  return false;
57
+ /*
58
+ * Timeouts were never retried: `isRetryableNetworkError` does not match
59
+ * `HttpClientTimeoutError`, so `retryOnNetworkError` had no effect on the
60
+ * most common transient failure.
61
+ */
62
+ if (error instanceof HttpClientTimeoutError) {
63
+ return retry.retryOnTimeout ?? retry.retryOnNetworkError ?? true;
64
+ }
65
+ if (!retry.retryOnNetworkError)
66
+ return false;
49
67
  return isRetryableNetworkError(error);
50
68
  }
51
69
  /**
@@ -71,15 +89,22 @@ function isRetryableNetworkError(error) {
71
89
  return !/body|disturbed|already (been )?(used|read)/i.test(error.message);
72
90
  }
73
91
  /**
74
- * Calculate retry delay with exponential backoff.
92
+ * Calculate the wait before retry number `attempt + 1`.
93
+ *
94
+ * The delay is `retryDelay` (fixed backoff) or `retryDelay * 2^attempt`
95
+ * (exponential), capped at `maxRetryDelay`. With `jitter` (the default) the
96
+ * wait is drawn uniformly from 0 up to that delay ("full jitter"), so a
97
+ * burst of clients that failed together does not retry in lockstep;
98
+ * `jitter: false` waits exactly the delay. Jitter used to add up to a
99
+ * fixed 1000 ms whatever `retryDelay` was, so `retryDelay: 50` could wait
100
+ * a second.
75
101
  */
76
102
  export function calculateRetryDelay(attempt, retry) {
77
103
  const base = retry.retryDelay ?? 1000;
78
104
  const max = retry.maxRetryDelay ?? 30_000;
79
105
  const ms = retry.backoff === "fixed" ? base : base * Math.pow(2, attempt);
80
- /* Clamp *after* jitter, and on the fixed branch too, or `maxRetryDelay`
81
- * is not actually a maximum. */
82
- return Math.min(ms + Math.random() * 1000, max);
106
+ const capped = Math.max(0, Math.min(ms, max));
107
+ return retry.jitter === false ? capped : Math.random() * capped;
83
108
  }
84
109
  /**
85
110
  * Delay for a given number of milliseconds.
@@ -8,14 +8,28 @@ export type HttpClientBody = BodyInit | Record<string, unknown> | readonly unkno
8
8
  export type HttpClientQueryValue = string | number | boolean | bigint | null | undefined | readonly (string | number | boolean | bigint)[];
9
9
  export type HttpClientQuery = Readonly<Record<string, HttpClientQueryValue>> | URLSearchParams;
10
10
  export type HttpResponseType = "auto" | "json" | "text" | "arrayBuffer" | "blob" | "response";
11
+ /**
12
+ * Retry policy for the HTTP client. See the README's "Retries and backoff".
13
+ */
11
14
  export interface HttpRetryOptions {
15
+ /** Retries after the first attempt (default 0: no retries). */
12
16
  readonly retries?: number;
17
+ /** Base delay in ms (default 1000). */
13
18
  readonly retryDelay?: number;
19
+ /** Upper bound on any single wait, in ms (default 30000). */
14
20
  readonly maxRetryDelay?: number;
21
+ /** Statuses that are retried (default 429, 502, 503, 504). */
15
22
  readonly retryStatusCodes?: readonly number[];
23
+ /** Methods that may be retried at all (default GET, HEAD, OPTIONS). */
16
24
  readonly retryMethods?: readonly HttpClientMethod[];
25
+ /** Retry transport failures such as a refused connection (default true). */
17
26
  readonly retryOnNetworkError?: boolean;
27
+ /** Retry a request that hit `timeout` (default: `retryOnNetworkError`). */
28
+ readonly retryOnTimeout?: boolean;
29
+ /** `"exponential"` doubles the delay per attempt (default); `"fixed"` does not. */
18
30
  readonly backoff?: "fixed" | "exponential";
31
+ /** Wait a random 0..delay instead of exactly the delay (default true). */
32
+ readonly jitter?: boolean;
19
33
  }
20
34
  export interface HttpClientResponse<T = unknown> {
21
35
  readonly data: T;
@@ -7,7 +7,7 @@
7
7
  * @module httpErrors/base
8
8
  */
9
9
  import { HttpError as BaseHttpError } from "@zudojs/errors";
10
- import { normalizeHeaders, getStatusText } from "./httpError.util.js";
10
+ import { defaultErrorCode, normalizeHeaders, getStatusText, } from "./httpError.util.js";
11
11
  /**
12
12
  * HTTP error with response-specific properties.
13
13
  *
@@ -31,7 +31,7 @@ export class HttpError extends BaseHttpError {
31
31
  const statusText = getStatusText(status);
32
32
  super(message ?? statusText, {
33
33
  statusCode: status,
34
- code: options.code,
34
+ code: options.code ?? defaultErrorCode(status),
35
35
  expose: options.expose ?? status < 500,
36
36
  metadata: options.metadata,
37
37
  cause: options.cause,
@@ -19,4 +19,12 @@ export declare function normalizeHeaders(headers: Record<string, string> | undef
19
19
  * `statusText` and `message` as "Unknown Status".
20
20
  */
21
21
  export declare function getStatusText(status: number): string;
22
+ /**
23
+ * The default error code for a status: its symbolic name, as the factories
24
+ * use (`415` gives `"UNSUPPORTED_MEDIA_TYPE"`, `404` `"NOT_FOUND"`).
25
+ * `undefined` for a status with no name, which keeps the shared default.
26
+ * `new HttpError(415, msg)` without a code used to report
27
+ * `ERR_OPERATION_FAILED`.
28
+ */
29
+ export declare function defaultErrorCode(status: number): string | undefined;
22
30
  //# sourceMappingURL=httpError.util.d.ts.map
@@ -8,6 +8,7 @@
8
8
  * @module httpErrors/util
9
9
  */
10
10
  import { getStatusText as lookupStatusText } from "../httpStatus/httpStatus.lookup.js";
11
+ import { statusName } from "../httpStatus/httpStatus.name.js";
11
12
  /**
12
13
  * Normalizes header keys to lowercase.
13
14
  */
@@ -33,4 +34,15 @@ export function normalizeHeaders(headers) {
33
34
  export function getStatusText(status) {
34
35
  return lookupStatusText(status);
35
36
  }
37
+ /**
38
+ * The default error code for a status: its symbolic name, as the factories
39
+ * use (`415` gives `"UNSUPPORTED_MEDIA_TYPE"`, `404` `"NOT_FOUND"`).
40
+ * `undefined` for a status with no name, which keeps the shared default.
41
+ * `new HttpError(415, msg)` without a code used to report
42
+ * `ERR_OPERATION_FAILED`.
43
+ */
44
+ export function defaultErrorCode(status) {
45
+ const name = statusName(status);
46
+ return name === "UNKNOWN" ? undefined : name;
47
+ }
36
48
  //# sourceMappingURL=httpError.util.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Serving web-standard fetch handlers from an `@zudojs/http` router.
3
+ */
4
+ import type { HttpFetchHandler, HttpFetchMountTarget, MountFetchHandlerOptions } from "./httpFetchMount.type.js";
5
+ /**
6
+ * Serves a web-standard `(request: Request) => Promise<Response>` handler —
7
+ * an `@zudojs/rpc` server, `@zudojs/api` operations, any fetch-style app —
8
+ * under `basePath` of an `@zudojs/http` router or router group.
9
+ *
10
+ * The handler receives a `Request` with the original method, headers
11
+ * (connection-scoped ones removed), body and query, and a `signal` that
12
+ * aborts when the client disconnects. Its `Response` is streamed back with
13
+ * status, status text and headers intact, every `Set-Cookie` kept separate.
14
+ * A handler that throws, or returns something that is not a `Response`,
15
+ * fails the request like any other route (500 unless the error carries a
16
+ * status).
17
+ *
18
+ * ```ts
19
+ * mountFetchHandler(router, "/rpc", createRPCFetchHandler(rpcServer));
20
+ * ```
21
+ *
22
+ * @returns A function that removes the mount.
23
+ */
24
+ export declare function mountFetchHandler(target: HttpFetchMountTarget, basePath: string, handler: HttpFetchHandler, options?: MountFetchHandlerOptions): () => void;
25
+ //# sourceMappingURL=httpFetchMount.core.d.ts.map
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Serving web-standard fetch handlers from an `@zudojs/http` router.
3
+ */
4
+ import { webResponseToContext } from "../httpResponse/httpResponse.fromWeb.js";
5
+ import { normalizeRoutePattern } from "../httpRouter/core/util/httpRoute.util.js";
6
+ import { contextUrl, toWebRequest } from "./httpFetchMount.request.js";
7
+ /** The wildcard every mount registers; its name is reserved for mounts. */
8
+ const MOUNT_WILDCARD = "*fetchMountPath";
9
+ function segments(path) {
10
+ return path.split("/").filter((segment) => segment !== "");
11
+ }
12
+ /**
13
+ * Removes the first `count` path segments, keeping a trailing slash.
14
+ *
15
+ * Counted in segments rather than characters so it holds for a mount under
16
+ * a group prefix, a parameterised mount path and a case-insensitive match.
17
+ */
18
+ function splitPrefix(pathname, count) {
19
+ const parts = segments(pathname);
20
+ const prefix = `/${parts.slice(0, count).join("/")}`;
21
+ const rest = parts.slice(count);
22
+ if (rest.length === 0)
23
+ return [prefix, "/"];
24
+ return [prefix, `/${rest.join("/")}${pathname.endsWith("/") ? "/" : ""}`];
25
+ }
26
+ /**
27
+ * Serves a web-standard `(request: Request) => Promise<Response>` handler —
28
+ * an `@zudojs/rpc` server, `@zudojs/api` operations, any fetch-style app —
29
+ * under `basePath` of an `@zudojs/http` router or router group.
30
+ *
31
+ * The handler receives a `Request` with the original method, headers
32
+ * (connection-scoped ones removed), body and query, and a `signal` that
33
+ * aborts when the client disconnects. Its `Response` is streamed back with
34
+ * status, status text and headers intact, every `Set-Cookie` kept separate.
35
+ * A handler that throws, or returns something that is not a `Response`,
36
+ * fails the request like any other route (500 unless the error carries a
37
+ * status).
38
+ *
39
+ * ```ts
40
+ * mountFetchHandler(router, "/rpc", createRPCFetchHandler(rpcServer));
41
+ * ```
42
+ *
43
+ * @returns A function that removes the mount.
44
+ */
45
+ export function mountFetchHandler(target, basePath, handler, options = {}) {
46
+ if (typeof handler !== "function") {
47
+ throw new TypeError("mountFetchHandler requires a handler function.");
48
+ }
49
+ const base = normalizeRoutePattern(basePath);
50
+ const pattern = base === "/" ? `/${MOUNT_WILDCARD}` : `${base}/${MOUNT_WILDCARD}`;
51
+ const strip = options.stripPrefix ?? true;
52
+ const routeHandler = async (context) => {
53
+ const url = contextUrl(context.request, options.origin);
54
+ const headers = {};
55
+ if (strip) {
56
+ const mounted = segments(context.route.path).length - 1;
57
+ const [prefix, rest] = splitPrefix(url.pathname, mounted);
58
+ url.pathname = rest;
59
+ if (mounted > 0)
60
+ headers["x-forwarded-prefix"] = prefix;
61
+ }
62
+ const request = toWebRequest(context.request, {
63
+ url,
64
+ signal: context.signal,
65
+ headers,
66
+ });
67
+ const response = await handler(request);
68
+ if (!(response instanceof Response)) {
69
+ throw new TypeError(`The fetch handler mounted at ${base} returned ${typeof response}, not a Response.`);
70
+ }
71
+ return webResponseToContext(response);
72
+ };
73
+ const routeOptions = {
74
+ openapi: options.openapi ?? false,
75
+ ...(options.name === undefined ? {} : { name: options.name }),
76
+ ...(options.middleware ? { middleware: options.middleware } : {}),
77
+ };
78
+ const removers = (options.methods ?? ["*"]).map((method) => target.on(method, pattern, routeHandler, routeOptions));
79
+ return () => {
80
+ for (const remove of removers)
81
+ remove();
82
+ };
83
+ }
84
+ //# sourceMappingURL=httpFetchMount.core.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Conversion of a request context into a WHATWG `Request`.
3
+ */
4
+ import type { HttpRequestContext } from "../httpRequest/httpRequest.context.js";
5
+ import type { ToWebRequestOptions } from "./httpFetchMount.type.js";
6
+ /**
7
+ * The context's URL as an absolute URL. The request target is never read
8
+ * as an authority; the origin is `origin` when given, else the context's.
9
+ */
10
+ export declare function contextUrl(context: HttpRequestContext, origin?: string): URL;
11
+ /**
12
+ * Builds a web `Request` from a request context.
13
+ *
14
+ * Headers are copied except connection-scoped ones; the body is the one the
15
+ * adapter read (bytes, text, a stream, or a parsed value re-encoded as JSON,
16
+ * labelled `application/json` unless it already had a JSON content type)
17
+ * and is never attached to `GET` / `HEAD`; the signal aborts the request
18
+ * when the client disconnects.
19
+ */
20
+ export declare function toWebRequest(context: HttpRequestContext, options?: ToWebRequestOptions): Request;
21
+ //# sourceMappingURL=httpFetchMount.request.d.ts.map
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Conversion of a request context into a WHATWG `Request`.
3
+ */
4
+ import { parseRequestTarget } from "../httpRequest/target/httpRequest.target.js";
5
+ /**
6
+ * Connection-scoped headers. They describe the hop between the client and
7
+ * this server, not the request, and `content-length` is recomputed from the
8
+ * body actually passed on.
9
+ */
10
+ const HOP_BY_HOP = new Set([
11
+ "connection",
12
+ "keep-alive",
13
+ "proxy-connection",
14
+ "transfer-encoding",
15
+ "te",
16
+ "trailer",
17
+ "upgrade",
18
+ "content-length",
19
+ ]);
20
+ const DEFAULT_ORIGIN = "http://localhost";
21
+ const JSON_TYPE = /^application\/(?:[\w.+-]+\+)?json(?:\s*;|$)/i;
22
+ /**
23
+ * The origin of the web request: `pinned` when the caller configured one,
24
+ * otherwise the context's own (from the `Host` header, or a trusted
25
+ * proxy's `X-Forwarded-Host`), otherwise `http://localhost`.
26
+ */
27
+ function resolveOrigin(context, pinned) {
28
+ for (const candidate of [pinned, context.origin, DEFAULT_ORIGIN]) {
29
+ if (candidate === undefined)
30
+ continue;
31
+ try {
32
+ return new URL(candidate).origin;
33
+ }
34
+ catch {
35
+ continue;
36
+ }
37
+ }
38
+ return DEFAULT_ORIGIN;
39
+ }
40
+ /**
41
+ * The context's URL as an absolute URL. The request target is never read
42
+ * as an authority; the origin is `origin` when given, else the context's.
43
+ */
44
+ export function contextUrl(context, origin) {
45
+ const target = parseRequestTarget(context.url);
46
+ const url = new URL(resolveOrigin(context, origin));
47
+ url.pathname = target.pathname;
48
+ url.search = target.search;
49
+ return url;
50
+ }
51
+ function toBody(body, headers) {
52
+ if (body === undefined || body === null)
53
+ return undefined;
54
+ if (typeof body === "string")
55
+ return body;
56
+ if (body instanceof Uint8Array)
57
+ return body;
58
+ if (body instanceof ArrayBuffer)
59
+ return body;
60
+ if (body instanceof ReadableStream ||
61
+ body instanceof Blob ||
62
+ body instanceof FormData ||
63
+ body instanceof URLSearchParams) {
64
+ return body;
65
+ }
66
+ if (!JSON_TYPE.test(headers.get("content-type") ?? "")) {
67
+ headers.set("content-type", "application/json");
68
+ }
69
+ return JSON.stringify(body);
70
+ }
71
+ /**
72
+ * Builds a web `Request` from a request context.
73
+ *
74
+ * Headers are copied except connection-scoped ones; the body is the one the
75
+ * adapter read (bytes, text, a stream, or a parsed value re-encoded as JSON,
76
+ * labelled `application/json` unless it already had a JSON content type)
77
+ * and is never attached to `GET` / `HEAD`; the signal aborts the request
78
+ * when the client disconnects.
79
+ */
80
+ export function toWebRequest(context, options = {}) {
81
+ const method = context.method.toUpperCase();
82
+ const headers = new Headers();
83
+ for (const [name, value] of Object.entries(context.headers)) {
84
+ if (!HOP_BY_HOP.has(name.toLowerCase()))
85
+ headers.append(name, value);
86
+ }
87
+ for (const [name, value] of Object.entries(options.headers ?? {})) {
88
+ headers.set(name, value);
89
+ }
90
+ const body = method === "GET" || method === "HEAD" ? undefined : toBody(context.body, headers);
91
+ const init = {
92
+ method,
93
+ headers,
94
+ ...(body === undefined ? {} : { body }),
95
+ ...(body instanceof ReadableStream ? { duplex: "half" } : {}),
96
+ ...(options.signal ? { signal: options.signal } : {}),
97
+ };
98
+ return new Request(options.url ?? contextUrl(context, options.origin), init);
99
+ }
100
+ //# sourceMappingURL=httpFetchMount.request.js.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Types for serving web-standard fetch handlers from a router.
3
+ */
4
+ import type { HttpMethod, HttpRouteOpenAPI, RouteOptions, RouterHandler } from "../httpRouter/core/types/httpRouter.type.js";
5
+ import type { HttpMiddleware } from "../httpMiddleware/httpMiddleware.type.js";
6
+ /** A web-standard handler: `Request` in, `Response` out. */
7
+ export type HttpFetchHandler = (request: Request) => Response | Promise<Response>;
8
+ /** Anything routes can be registered on: an `HttpRouter` or a router group. */
9
+ export interface HttpFetchMountTarget {
10
+ on(method: HttpMethod | "*", path: string, handler: RouterHandler, options?: RouteOptions): () => void;
11
+ }
12
+ /** Options for `mountFetchHandler`. */
13
+ export interface MountFetchHandlerOptions {
14
+ /** Methods routed to the handler. Default: every method. */
15
+ readonly methods?: readonly HttpMethod[];
16
+ /**
17
+ * Remove the mount path from the URL the handler sees, so a handler
18
+ * mounted at `/rpc` receives `/users.get` for `/rpc/users.get`. The
19
+ * removed prefix is passed as `x-forwarded-prefix`. Default: `true`.
20
+ */
21
+ readonly stripPrefix?: boolean;
22
+ /**
23
+ * Origin of the handler's `request.url`, e.g. `https://api.example.com`.
24
+ * When set it is used for every request. When not, the origin comes from
25
+ * the request — its `Host` header, or `X-Forwarded-Host` from a trusted
26
+ * proxy — which the client chooses; set this whenever the handler builds
27
+ * absolute URLs (redirects, callback or reset links) or checks
28
+ * `Origin` against its own. Falls back to `http://localhost`.
29
+ */
30
+ readonly origin?: string;
31
+ /** Middleware run before the handler. */
32
+ readonly middleware?: readonly HttpMiddleware[];
33
+ /** Route name. */
34
+ readonly name?: string;
35
+ /**
36
+ * OpenAPI documentation for the mount. Default: `false` — a mounted
37
+ * handler documents its own operations (see `@zudojs/openapi`'s
38
+ * `createOpenAPIDocumentFromRoutes`).
39
+ */
40
+ readonly openapi?: HttpRouteOpenAPI;
41
+ }
42
+ /** Options for `toWebRequest`. */
43
+ export interface ToWebRequestOptions {
44
+ /** Overrides the request URL (absolute). */
45
+ readonly url?: string | URL;
46
+ /** Aborts the web request; normally the router context's signal. */
47
+ readonly signal?: AbortSignal;
48
+ /**
49
+ * Origin of the web request. Overrides the context's protocol and host
50
+ * (which come from the client's `Host` header) when set.
51
+ */
52
+ readonly origin?: string;
53
+ /** Extra headers set on the web request, replacing same-named ones. */
54
+ readonly headers?: Readonly<Record<string, string>>;
55
+ }
56
+ //# sourceMappingURL=httpFetchMount.type.d.ts.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Types for serving web-standard fetch handlers from a router.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=httpFetchMount.type.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @zudojs/http/httpFetchMount
3
+ *
4
+ * Serves web-standard fetch handlers (`Request` → `Response`) from an
5
+ * `@zudojs/http` router: `mountFetchHandler` registers one under a path,
6
+ * `toWebRequest` converts a request context into a `Request` on its own.
7
+ */
8
+ export { mountFetchHandler } from "./httpFetchMount.core.js";
9
+ export { toWebRequest, contextUrl } from "./httpFetchMount.request.js";
10
+ export type { HttpFetchHandler, HttpFetchMountTarget, MountFetchHandlerOptions, ToWebRequestOptions, } from "./httpFetchMount.type.js";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @zudojs/http/httpFetchMount
3
+ *
4
+ * Serves web-standard fetch handlers (`Request` → `Response`) from an
5
+ * `@zudojs/http` router: `mountFetchHandler` registers one under a path,
6
+ * `toWebRequest` converts a request context into a `Request` on its own.
7
+ */
8
+ export { mountFetchHandler } from "./httpFetchMount.core.js";
9
+ export { toWebRequest, contextUrl } from "./httpFetchMount.request.js";
10
+ //# sourceMappingURL=index.js.map
@@ -7,6 +7,7 @@
7
7
  * Transport-specific socket management belongs to the server/client adapter
8
8
  * layer. This module only handles HTTP-level semantics.
9
9
  */
10
+ import { assertSafeHeaderValue, escapeHeaderQuotedString, } from "../httpHeaders/security/index.js";
10
11
  /* -------------------------------------------------------------------------- */
11
12
  /* Constants */
12
13
  /* -------------------------------------------------------------------------- */
@@ -121,7 +122,9 @@ export function formatKeepAliveHeader(parameters = {}) {
121
122
  parts.push(`${key}=${quoteIfNeeded(value)}`);
122
123
  }
123
124
  }
124
- return parts.join(", ");
125
+ const header = parts.join(", ");
126
+ assertSafeHeaderValue(header);
127
+ return header;
125
128
  }
126
129
  /* -------------------------------------------------------------------------- */
127
130
  /* Connection Header */
@@ -348,10 +351,19 @@ function unquote(value) {
348
351
  }
349
352
  return trimmed;
350
353
  }
354
+ /**
355
+ * Emits a Keep-Alive parameter value.
356
+ *
357
+ * Quoting does not neutralise a CR or LF, so the control character used to
358
+ * survive into the field value. `escapeHeaderQuotedString` rejects it, the
359
+ * same helper every other quoted-parameter emitter in this package uses.
360
+ *
361
+ * @throws {TypeError} If the value contains a forbidden control character.
362
+ */
351
363
  function quoteIfNeeded(value) {
352
364
  if (/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(value)) {
353
365
  return value;
354
366
  }
355
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
367
+ return `"${escapeHeaderQuotedString(value)}"`;
356
368
  }
357
369
  //# sourceMappingURL=httpKeepAlive.core.js.map