@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
package/README.md CHANGED
@@ -33,6 +33,10 @@ const server = createHttpServer({
33
33
  await server.start();
34
34
  ```
35
35
 
36
+ `createHttpServer` takes `HttpServerOptions`, so `request` in the handler
37
+ above is typed `HttpRequestContext` without an annotation (the options used
38
+ to be typed `unknown`, which failed `strict` builds with TS7006).
39
+
36
40
  A handler receives an `HttpRequestContext` and may return an
37
41
  `HttpResponseContext` or any JSON value. **Every value that is not an
38
42
  `HttpResponseContext` is data**: a plain object such as `{ status: "ok" }` is
@@ -72,6 +76,15 @@ answers 502 and never exposes the cause.
72
76
  wraps `@zudojs/security`'s `createRateLimiter`. Requests with no usable
73
77
  client address share one bucket (`UNKNOWN_CLIENT_RATE_LIMIT_IP`,
74
78
  `0.0.0.0`): they are limited together, never unlimited and never a 500.
79
+ The 429 carries a JSON body sent as `application/json` and always a
80
+ `Retry-After` header, even when a custom limiter handler omits it.
81
+ - **Request ids.** `request.id` reuses the client's `x-request-id` when it is
82
+ 1-128 characters of `[A-Za-z0-9._:-]`; any other value is ignored and a
83
+ UUID is generated, so an id copied into logs can never carry spaces,
84
+ quotes or control characters. `createNodeHttpAdapter({ trustRequestId:
85
+ false })` always generates one. The request guard's own `X-Request-Id`
86
+ check (letters, digits, `_`, `-`) still answers 400 to a malformed header
87
+ first unless it is tuned or turned off.
75
88
  - Signed-cookie signatures are compared with `@zudojs/crypto`'s constant-time
76
89
  `timingSafeEqualString`.
77
90
  - Contexts built by the stock adapters log through a `@zudojs/logger` console
@@ -82,12 +95,220 @@ answers 502 and never exposes the cause.
82
95
  - `HttpServer.stop()` gives in-flight requests the full
83
96
  `gracefulShutdownTimeout`.
84
97
 
98
+ ## Routes
99
+
100
+ A route handler returns what a server handler returns:
101
+
102
+ ```typescript
103
+ router.get("/health", () => ({ status: "ok" })); // 200, JSON body
104
+ router.get("/users/:id", async (ctx) => loadUser(ctx.params.id));
105
+ router.post("/users", () =>
106
+ createResponseContext({ status: 201, body: { created: true } }),
107
+ );
108
+ router.delete("/users/:id", () => undefined); // 204
109
+ ```
110
+
111
+ A plain value (object, array, string, number, boolean) is sent as `200`
112
+ with a JSON body; `undefined` or `null` is `204 No Content`; an
113
+ `HttpResponseContext` or a web `Response` is sent as built. (A plain
114
+ object used to be a type error and was sent as an empty `204`.) The
115
+ router and `RouteDispatcher` behave the same.
116
+
117
+ Route parameters are set on the request before route middleware runs, so
118
+ `ctx.request.getParam("id")` works in a guard or an `extractResource`
119
+ loader as well as in the handler (`ctx.params`).
120
+
121
+ ## Middleware errors
122
+
123
+ An error thrown by a middleware or handler propagates **as the error that
124
+ was thrown**. An outer middleware's `await next()` rejects with it, the
125
+ pipeline's `onError` receives it, and so does the server's `errorHandler`,
126
+ so `error instanceof NotFoundError` works in each. It used to arrive wrapped
127
+ in `HttpMiddlewareError` (inside a middleware) or
128
+ `HttpMiddlewarePipelineError` (in `errorHandler`), with the original only in
129
+ `cause` / `errors[0].cause`.
130
+
131
+ Code after `await next()` does not run when the chain below it throws,
132
+ unless the middleware catches the error:
133
+
134
+ ```typescript
135
+ pipeline.use(async (ctx, next) => {
136
+ const started = Date.now();
137
+ try {
138
+ return await next();
139
+ } finally {
140
+ log.info("request", { path: ctx.request.path, ms: Date.now() - started });
141
+ }
142
+ });
143
+ ```
144
+
145
+ If `onError` returns a response, that is the recovery; if it throws, what
146
+ it threw propagates (rethrow the error to pass it on, or throw a different
147
+ one to translate it).
148
+
149
+ `new HttpError(415, "No XML")` without a `code` gets its code from the
150
+ status (`"UNSUPPORTED_MEDIA_TYPE"`, `"NOT_FOUND"`, ...), matching the
151
+ `notFound()`-style factories, instead of `ERR_OPERATION_FAILED`.
152
+
153
+ ## HTTP client: retries and backoff
154
+
155
+ ```typescript
156
+ const client = new HttpClient({
157
+ timeout: 5_000,
158
+ retry: { retries: 3, retryDelay: 200, maxRetryDelay: 5_000 },
159
+ });
160
+ ```
161
+
162
+ - **What is retried:** responses with a status in `retryStatusCodes`
163
+ (default 429, 502, 503, 504); transport failures such as a refused
164
+ connection (`retryOnNetworkError`, default `true`); and requests that hit
165
+ `timeout` (`retryOnTimeout`, default: the `retryOnNetworkError` value).
166
+ Timeouts used to be excluded, so a `GET` with retries still failed on
167
+ the first timeout. Aborting through your own `signal` is never retried.
168
+ - **Which methods:** only `retryMethods` (default `GET`, `HEAD`,
169
+ `OPTIONS`). A `POST` that timed out may already have been processed, so
170
+ it is not replayed unless you list it.
171
+ - **Backoff:** the delay is `retryDelay` (default 1000 ms) times
172
+ `2^attempt` with `backoff: "exponential"` (the default), or `retryDelay`
173
+ every time with `"fixed"`, capped at `maxRetryDelay` (default 30 s).
174
+ - **Jitter:** each wait is drawn uniformly between 0 and that delay (full
175
+ jitter), so clients that failed together do not retry in lockstep.
176
+ `jitter: false` waits exactly the delay. Jitter used to add up to a fixed
177
+ second regardless of `retryDelay`.
178
+ - `retries` counts retries after the first attempt (default 0).
179
+
180
+ ## OpenAPI from your routes
181
+
182
+ Routes carry their own documentation through the `openapi` option, and the
183
+ document is generated from the routes the router actually registered — no
184
+ second list to keep in sync. Schemas may be `@zudojs/schema` schemas or raw
185
+ OpenAPI schemas.
186
+
187
+ ```typescript
188
+ import { objectSchema, stringSchema, numberSchema, optionalSchema } from "@zudojs/schema";
189
+ import { createRouter, generateOpenAPIDocument, mountOpenAPI } from "@zudojs/http";
190
+
191
+ const user = objectSchema({ id: stringSchema().uuid(), name: stringSchema() });
192
+
193
+ const router = createRouter();
194
+ router.get("/users/:id", getUser, {
195
+ openapi: {
196
+ summary: "Get a user",
197
+ tags: ["users"],
198
+ params: objectSchema({ id: stringSchema().uuid() }),
199
+ responses: { "200": { schema: user }, "404": { description: "No such user" } },
200
+ },
201
+ });
202
+ router.get("/users", listUsers, {
203
+ openapi: { query: objectSchema({ limit: optionalSchema(numberSchema().int()) }) },
204
+ });
205
+ router.post("/users", createUser, {
206
+ openapi: { body: objectSchema({ name: stringSchema() }), responses: { "201": { schema: user } } },
207
+ });
208
+ router.get("/health", health, { openapi: false }); // never documented
209
+
210
+ // One-off document:
211
+ const document = generateOpenAPIDocument(router, {
212
+ info: { title: "Users API", version: "1.0.0" },
213
+ exclude: ["/internal/*"],
214
+ validate: true,
215
+ });
216
+
217
+ // Or serve it: GET /openapi.json and a Swagger UI page at GET /docs.
218
+ mountOpenAPI(router, {
219
+ info: { title: "Users API", version: "1.0.0" },
220
+ yamlPath: "/openapi.yaml", // optional
221
+ ui: { renderer: "redoc" }, // optional; any renderOpenAPIUI option
222
+ });
223
+ ```
224
+
225
+ - `:id` and `{id}` become `{id}`; every template slot is documented even when
226
+ nothing declares it. A regex-constrained `:id(\d+)` becomes a parameter
227
+ with that `pattern`, an optional `:id?` is documented as both paths, and a
228
+ wildcard `*rest` becomes a `{rest}` slot (`wildcards: "exclude"` drops such
229
+ routes instead).
230
+ - Left out: `all()` routes, `CONNECT`, routes with `openapi: false` or
231
+ `{ hidden: true }`, and anything matched by `exclude` (exact path,
232
+ `"/prefix/*"`, a `RegExp`, or a predicate). The router's automatic `HEAD`
233
+ and `OPTIONS` answers are not registered routes and never appear.
234
+ `undocumented: "exclude"` documents only routes that declare `openapi`.
235
+ - Router groups pass `openapi` defaults to their routes: tags are unioned,
236
+ everything else is overridden by the route.
237
+ - The document follows the router: a route added later is in the next
238
+ `generateOpenAPIDocument` call and the next request to a mounted
239
+ `/openapi.json`. `createRouterOpenAPI(router, options)` gives the
240
+ underlying `OpenAPIManager`, re-read only when the route table changed.
241
+ - Paths are configurable (`path`, `docsPath: false` to disable the page,
242
+ `ui.specUrl` when served under a prefix), and `middleware` protects the
243
+ documentation routes.
244
+
245
+ ## Mounting web-standard handlers
246
+
247
+ `mountFetchHandler` serves any `(request: Request) => Response | Promise<Response>`
248
+ handler — an `@zudojs/rpc` server, `@zudojs/api` operations, another
249
+ fetch-style app — under a path of a router or router group.
250
+
251
+ ```typescript
252
+ import { mountFetchHandler } from "@zudojs/http";
253
+ import { createRPCFetchHandler } from "@zudojs/rpc";
254
+
255
+ const unmount = mountFetchHandler(router, "/rpc", createRPCFetchHandler(rpcServer));
256
+ // { methods: ["POST"], stripPrefix: false, middleware: [auth] } are optional
257
+ ```
258
+
259
+ The handler sees the original method, query, headers (connection-scoped ones
260
+ removed) and body; the mount path is stripped from its URL by default and
261
+ passed as `x-forwarded-prefix`. Its `Response` is streamed back with status,
262
+ status text and headers intact, each `Set-Cookie` kept separate. The
263
+ request's `signal` aborts when the client disconnects. A handler that throws
264
+ or returns something other than a `Response` fails the request like any
265
+ route (a generic 500 unless the error carries a status). `toWebRequest(context)`
266
+ does the request conversion on its own.
267
+
268
+ The origin of the handler's `request.url` comes from the client's `Host`
269
+ header (or `X-Forwarded-Host` from a trusted proxy) unless you pin it with
270
+ `{ origin: "https://api.example.com" }`. Pin it whenever the handler builds
271
+ absolute URLs or compares `Origin` against its own.
272
+
273
+ Every Node request context now carries that signal too: `request.signal` and
274
+ the router's `ctx.signal` abort when the client goes away, and a streamed
275
+ response body stops being read.
276
+
277
+ ## Guards that refuse a request
278
+
279
+ A middleware answers a request itself — 401, 403, 404 — by returning a
280
+ `GuardResponse` from `@zudojs/middleware`. The router, `HttpMiddlewarePipeline`
281
+ and `RouteDispatcher` send it with its own status, headers and body; headers an
282
+ outer middleware already set (CORS, for instance) are kept.
283
+
284
+ ```typescript
285
+ import { createGuardResponse } from "@zudojs/middleware";
286
+ import { authorize } from "@zudojs/permissions";
287
+
288
+ router.delete("/posts/:id", deletePost, {
289
+ middleware: [
290
+ authorize(engine, "post:delete", { extractActor }), // 401 / 403
291
+ async (ctx, next) =>
292
+ ctx.request.getHeader("x-confirm")
293
+ ? next()
294
+ : createGuardResponse({ status: 400, body: { error: "Confirm first" } }),
295
+ ],
296
+ });
297
+ ```
298
+
299
+ A route middleware's return value used to be ignored unless it was an
300
+ `HttpResponseContext` or a web `Response`, so `authorize()` and the tenancy
301
+ middleware refused requests with `200`. Only the branded object is honoured: an ordinary
302
+ object with a `status` key keeps its old meaning.
303
+
85
304
  ## Features
86
305
 
87
306
  - Runtime-independent HTTP server abstraction
88
307
  - Request/response wrappers with full Web API compatibility
89
308
  - Middleware pipeline with error handling
90
309
  - Router with parameter extraction
310
+ - OpenAPI documents generated from the registered routes
311
+ - Mounting of web-standard fetch handlers
91
312
  - CORS, security headers, and content negotiation
92
313
  - HTTP client with interceptors
93
314
 
@@ -1,5 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { HTTPContext, HTTPHandler, HTTPMiddleware, HTTPRequest, HTTPResponse, HTTPRouteMatch, HTTPState } from "../httpTypes/http.types.js";
3
+ import type { TrustProxy } from "../httpTrustProxy/httpTrustProxy.type.js";
3
4
  export interface HTTPAdapter<State extends HTTPState = HTTPState> {
4
5
  readonly name: string;
5
6
  createRequest(request: IncomingMessage): HTTPRequest;
@@ -9,9 +10,21 @@ export interface HTTPAdapter<State extends HTTPState = HTTPState> {
9
10
  readonly signal?: AbortSignal;
10
11
  }): HTTPContext<State>;
11
12
  }
13
+ /**
14
+ * Options every Node request built by this module accepts.
15
+ *
16
+ * `trustProxy` defaults to `false`, so `X-Forwarded-For` and
17
+ * `X-Forwarded-Proto` are ignored and the socket peer decides `request.ip`,
18
+ * `request.protocol` and `request.secure`.
19
+ */
20
+ export interface NodeRequestTrustOptions {
21
+ readonly trustProxy?: TrustProxy;
22
+ }
12
23
  export declare class NodeHTTPAdapter<State extends HTTPState = HTTPState> implements HTTPAdapter<State> {
13
24
  readonly name = "node";
14
- createRequest(request: IncomingMessage): HTTPRequest;
25
+ private readonly trustProxy;
26
+ constructor(options?: NodeRequestTrustOptions);
27
+ createRequest(request: IncomingMessage, options?: NodeRequestTrustOptions): HTTPRequest;
15
28
  createResponse(response: ServerResponse): HTTPResponse;
16
29
  createContext(request: HTTPRequest, response: HTTPResponse, options?: {
17
30
  readonly state?: State;
@@ -20,12 +33,19 @@ export declare class NodeHTTPAdapter<State extends HTTPState = HTTPState> implem
20
33
  }
21
34
  export interface HTTPAdapterOptions<State extends HTTPState = HTTPState> {
22
35
  readonly adapter?: HTTPAdapter<State>;
36
+ /**
37
+ * Which socket peers may speak for a client through `X-Forwarded-*`.
38
+ * Defaults to `false` — forwarded headers are ignored.
39
+ */
40
+ readonly trustProxy?: TrustProxy;
23
41
  }
24
- export declare function createHTTPAdapter<State extends HTTPState = HTTPState>(): NodeHTTPAdapter<State>;
42
+ export declare function createHTTPAdapter<State extends HTTPState = HTTPState>(options?: NodeRequestTrustOptions): NodeHTTPAdapter<State>;
25
43
  export interface RequestAdapter {
26
44
  toHTTPRequest(request: IncomingMessage): HTTPRequest;
27
45
  }
28
46
  export declare class NodeRequestAdapter implements RequestAdapter {
47
+ private readonly trustProxy;
48
+ constructor(options?: NodeRequestTrustOptions);
29
49
  toHTTPRequest(request: IncomingMessage): HTTPRequest;
30
50
  }
31
51
  export interface ResponseAdapter {
@@ -61,11 +81,12 @@ export declare class DefaultHTTPMiddlewareAdapter<State extends HTTPState = HTTP
61
81
  export interface HTTPRouteAdapter<State extends HTTPState = HTTPState> {
62
82
  match(method: string, path: string): HTTPRouteMatch<State> | undefined;
63
83
  }
64
- export declare function adaptNodeRequest(request: IncomingMessage): HTTPRequest;
84
+ export declare function adaptNodeRequest(request: IncomingMessage, options?: NodeRequestTrustOptions): HTTPRequest;
65
85
  export declare function adaptNodeResponse(response: ServerResponse): HTTPResponse;
66
86
  export declare function adaptNodeContext<State extends HTTPState = HTTPState>(request: IncomingMessage, response: ServerResponse, options?: {
67
87
  readonly state?: State;
68
88
  readonly signal?: AbortSignal;
89
+ readonly trustProxy?: TrustProxy;
69
90
  }): HTTPContext<State>;
70
91
  export declare function isHTTPAdapter(value: unknown): value is HTTPAdapter;
71
92
  //# sourceMappingURL=http.adapters.d.ts.map
@@ -2,13 +2,16 @@ import { createHTTPContext } from "../httpContext/http.context.js";
2
2
  import { createHTTPRequest } from "../httpRequest/http.request.js";
3
3
  import { createHTTPResponse } from "../httpResponse/http.response.js";
4
4
  import { createDefaultContextLogger } from "./httpAdapter.logger.js";
5
- /* -------------------------------------------------------------------------- */
6
- /* Node Adapter */
7
- /* -------------------------------------------------------------------------- */
8
5
  export class NodeHTTPAdapter {
9
6
  name = "node";
10
- createRequest(request) {
11
- return createHTTPRequest(request);
7
+ trustProxy;
8
+ constructor(options = {}) {
9
+ this.trustProxy = options.trustProxy ?? false;
10
+ }
11
+ createRequest(request, options = {}) {
12
+ return createHTTPRequest(request, {
13
+ trustProxy: options.trustProxy ?? this.trustProxy,
14
+ });
12
15
  }
13
16
  createResponse(response) {
14
17
  return createHTTPResponse(response);
@@ -26,12 +29,16 @@ export class NodeHTTPAdapter {
26
29
  /* -------------------------------------------------------------------------- */
27
30
  /* Adapter Factory */
28
31
  /* -------------------------------------------------------------------------- */
29
- export function createHTTPAdapter() {
30
- return new NodeHTTPAdapter();
32
+ export function createHTTPAdapter(options = {}) {
33
+ return new NodeHTTPAdapter(options);
31
34
  }
32
35
  export class NodeRequestAdapter {
36
+ trustProxy;
37
+ constructor(options = {}) {
38
+ this.trustProxy = options.trustProxy ?? false;
39
+ }
33
40
  toHTTPRequest(request) {
34
- return createHTTPRequest(request);
41
+ return createHTTPRequest(request, { trustProxy: this.trustProxy });
35
42
  }
36
43
  }
37
44
  export class NodeResponseAdapter {
@@ -76,15 +83,17 @@ export class DefaultHTTPMiddlewareAdapter {
76
83
  /* -------------------------------------------------------------------------- */
77
84
  /* Request Conversion Helpers */
78
85
  /* -------------------------------------------------------------------------- */
79
- export function adaptNodeRequest(request) {
80
- return createHTTPRequest(request);
86
+ export function adaptNodeRequest(request, options = {}) {
87
+ return createHTTPRequest(request, {
88
+ trustProxy: options.trustProxy ?? false,
89
+ });
81
90
  }
82
91
  export function adaptNodeResponse(response) {
83
92
  return createHTTPResponse(response);
84
93
  }
85
94
  export function adaptNodeContext(request, response, options = {}) {
86
95
  return createHTTPContext({
87
- request: adaptNodeRequest(request),
96
+ request: adaptNodeRequest(request, { trustProxy: options.trustProxy }),
88
97
  response: adaptNodeResponse(response),
89
98
  state: options.state ?? {},
90
99
  signal: options.signal,
@@ -22,6 +22,7 @@ export declare class NodeHttpAdapter extends BaseHttpAdapter {
22
22
  private readonly keepAliveTimeout;
23
23
  private readonly connectionTimeout;
24
24
  private readonly trustProxy;
25
+ private readonly trustRequestId;
25
26
  private readonly maxConnections;
26
27
  private readonly connectionsCheckingInterval;
27
28
  private readonly shutdownGraceMs;
@@ -39,7 +40,7 @@ export declare class NodeHttpAdapter extends BaseHttpAdapter {
39
40
  constructor(options?: NodeAdapterOptions);
40
41
  get httpServer(): Server | undefined;
41
42
  get address(): NodeServerAddress | undefined;
42
- createRequest(input: unknown): HttpRequestContext;
43
+ createRequest(input: unknown, signal?: AbortSignal): HttpRequestContext;
43
44
  createResponse(input?: unknown): HttpResponseContext;
44
45
  createWriter(response: unknown): HttpResponseWriter;
45
46
  handle(input: unknown): Promise<void>;
@@ -30,6 +30,7 @@ export class NodeHttpAdapter extends BaseHttpAdapter {
30
30
  keepAliveTimeout;
31
31
  connectionTimeout;
32
32
  trustProxy;
33
+ trustRequestId;
33
34
  maxConnections;
34
35
  connectionsCheckingInterval;
35
36
  shutdownGraceMs;
@@ -76,6 +77,7 @@ export class NodeHttpAdapter extends BaseHttpAdapter {
76
77
  Math.min(30_000, this.headersTimeout);
77
78
  this.shutdownGraceMs = options.shutdownGraceMs;
78
79
  this.trustProxy = options.trustProxy;
80
+ this.trustRequestId = options.trustRequestId ?? true;
79
81
  if (options.trustProxy !== undefined) {
80
82
  compileTrustProxy(options.trustProxy);
81
83
  }
@@ -109,13 +111,15 @@ export class NodeHttpAdapter extends BaseHttpAdapter {
109
111
  /* ------------------------------------------------------------------------ */
110
112
  /* Request / Response */
111
113
  /* ------------------------------------------------------------------------ */
112
- createRequest(input) {
114
+ createRequest(input, signal) {
113
115
  if (!isIncomingMessage(input)) {
114
116
  throw new TypeError("NodeHttpAdapter.createRequest expected an IncomingMessage.");
115
117
  }
116
118
  return createNodeRequestContext(input, {
117
119
  maxBodySize: this.maxBodySize,
118
120
  trustProxy: this.trustProxy,
121
+ trustRequestId: this.trustRequestId,
122
+ signal,
119
123
  });
120
124
  }
121
125
  createResponse(input) {
@@ -144,8 +148,19 @@ export class NodeHttpAdapter extends BaseHttpAdapter {
144
148
  return;
145
149
  }
146
150
  let context;
151
+ const disconnect = new AbortController();
152
+ /*
153
+ * `close` before the response finished means the client went away.
154
+ * Handlers see it as `request.signal` / the router's `ctx.signal`, and a
155
+ * streamed response body stops being pulled.
156
+ */
157
+ response.once("close", () => {
158
+ if (!response.writableFinished) {
159
+ disconnect.abort();
160
+ }
161
+ });
147
162
  try {
148
- context = this.createRequest(request);
163
+ context = this.createRequest(request, disconnect.signal);
149
164
  }
150
165
  catch (error) {
151
166
  /*
@@ -6,8 +6,9 @@
6
6
  import { HttpRequestContext, createRequestContext, } from "../../httpRequest/httpRequest.context.js";
7
7
  import { getClientIp, isTrustedProxy, } from "../../httpTrustProxy/httpTrustProxy.core.js";
8
8
  import { removePort, extractPort } from "./httpNode.server.js";
9
- import { parseQueryString } from "../../httpQuery/http.query.js";
9
+ import { parseQueryString } from "../../httpQuery/index.js";
10
10
  import { findRequestTargetViolation } from "../../httpRequest/target/httpRequest.target.js";
11
+ import { resolveIncomingRequestId } from "../../httpRequest/requestId/httpRequest.requestId.js";
11
12
  /* -------------------------------------------------------------------------- */
12
13
  /* Proxy Trust */
13
14
  /* -------------------------------------------------------------------------- */
@@ -159,7 +160,11 @@ export function createNodeRequestContext(request, options = {}) {
159
160
  const port = getNodeRequestPort(request, options);
160
161
  const remoteAddress = getNodeRemoteAddress(request, options);
161
162
  const query = parseNodeQuery(request);
163
+ const id = options.trustRequestId === false
164
+ ? undefined
165
+ : resolveIncomingRequestId(request.headers["x-request-id"]);
162
166
  return createRequestContext({
167
+ ...(id === undefined ? {} : { id }),
163
168
  method: request.method?.toUpperCase() ?? "GET",
164
169
  url,
165
170
  protocol,
@@ -168,6 +173,7 @@ export function createNodeRequestContext(request, options = {}) {
168
173
  headers,
169
174
  query,
170
175
  remoteAddress,
176
+ signal: options.signal,
171
177
  });
172
178
  }
173
179
  //# sourceMappingURL=httpNode.request.js.map
@@ -13,6 +13,11 @@ export interface NodeAdapterOptions extends HttpAdapterOptions, NodeAdapterSecur
13
13
  readonly server?: Server;
14
14
  readonly maxBodySize?: number;
15
15
  readonly trustProxy?: boolean | number | string | readonly string[];
16
+ /**
17
+ * Whether `request.id` reuses a well-formed incoming `x-request-id`
18
+ * header (default: `true`); see {@link NodeRequestOptions.trustRequestId}.
19
+ */
20
+ readonly trustRequestId?: boolean;
16
21
  readonly requestTimeout?: number;
17
22
  readonly headersTimeout?: number;
18
23
  readonly keepAliveTimeout?: number;
@@ -54,6 +59,15 @@ export interface NodeRequestOptions {
54
59
  * - string[]: Trust specific IP addresses
55
60
  */
56
61
  readonly trustProxy?: TrustProxy;
62
+ /** Signal carried by the context; aborted when the client disconnects. */
63
+ readonly signal?: AbortSignal;
64
+ /**
65
+ * Whether `request.id` reuses the client's `x-request-id` header
66
+ * (default: `true`). The header is used only when it is at most 128
67
+ * characters of `[A-Za-z0-9._:-]`; any other value is ignored and an id
68
+ * is generated. `false` always generates one.
69
+ */
70
+ readonly trustRequestId?: boolean;
57
71
  }
58
72
  export interface NodeServerAddress {
59
73
  readonly host: string;
@@ -46,9 +46,28 @@ export interface AgentRegistryKey {
46
46
  readonly name?: string;
47
47
  }
48
48
  export declare function getOrCreateAgent(key: string | AgentRegistryKey, options?: HTTPAgentConfig | HTTPSAgentConfig): HTTPAgentInstance;
49
- export declare function getAgent(key: string | AgentRegistryKey): HTTPAgentInstance | undefined;
50
- export declare function hasAgent(key: string | AgentRegistryKey): boolean;
51
- export declare function removeAgent(key: string | AgentRegistryKey, destroy?: boolean): boolean;
49
+ /**
50
+ * Looks up a registered agent.
51
+ *
52
+ * `options` must be the same TLS-relevant options `getOrCreateAgent` was
53
+ * given, because they are part of the key; omitting them looks up the agent
54
+ * created with no options.
55
+ */
56
+ export declare function getAgent(key: string | AgentRegistryKey, options?: HTTPAgentConfig | HTTPSAgentConfig): HTTPAgentInstance | undefined;
57
+ /**
58
+ * Whether an agent is registered for this key and TLS option set.
59
+ */
60
+ export declare function hasAgent(key: string | AgentRegistryKey, options?: HTTPAgentConfig | HTTPSAgentConfig): boolean;
61
+ /**
62
+ * Removes registered agents for a key.
63
+ *
64
+ * With `options` the single matching agent is removed; without them every
65
+ * agent registered for that host is removed, whatever TLS options it was
66
+ * created with, so a per-host teardown releases all of its sockets.
67
+ *
68
+ * @returns Whether anything was removed.
69
+ */
70
+ export declare function removeAgent(key: string | AgentRegistryKey, destroy?: boolean, options?: HTTPAgentConfig | HTTPSAgentConfig): boolean;
52
71
  export declare function clearAgents(destroy?: boolean): void;
53
72
  export declare function getRegisteredAgentKeys(): string[];
54
73
  export declare function getDefaultHTTPAgent(): HTTPAgent;
@@ -113,7 +113,7 @@ export function getOrCreateAgent(key, options = {}) {
113
113
  * passing `rejectUnauthorized: false` would have disabled certificate
114
114
  * verification for a caller that had pinned a CA, with no way to detect it.
115
115
  */
116
- const cacheKey = `${registryKey}|${tlsFingerprint(options)}`;
116
+ const cacheKey = agentCacheKey(registryKey, options);
117
117
  const existing = agentRegistry.get(cacheKey);
118
118
  if (existing) {
119
119
  return existing;
@@ -127,6 +127,18 @@ export function getOrCreateAgent(key, options = {}) {
127
127
  agentRegistry.set(cacheKey, agent);
128
128
  return agent;
129
129
  }
130
+ /**
131
+ * Builds the registry key an agent is stored under.
132
+ *
133
+ * Every read and write goes through this one helper. `getOrCreateAgent` used
134
+ * to append the TLS fingerprint while `getAgent`, `hasAgent` and
135
+ * `removeAgent` looked up the bare registry key, so every lookup missed and
136
+ * the documented per-host teardown silently leaked the agent and its
137
+ * keep-alive sockets.
138
+ */
139
+ function agentCacheKey(registryKey, options) {
140
+ return `${registryKey}|${tlsFingerprint(options)}`;
141
+ }
130
142
  /**
131
143
  * Builds a stable fingerprint of the TLS-relevant fields of an agent config.
132
144
  */
@@ -156,23 +168,49 @@ function describeCredential(value) {
156
168
  }
157
169
  return "opaque";
158
170
  }
159
- export function getAgent(key) {
160
- return agentRegistry.get(normalizeRegistryKey(key));
171
+ /**
172
+ * Looks up a registered agent.
173
+ *
174
+ * `options` must be the same TLS-relevant options `getOrCreateAgent` was
175
+ * given, because they are part of the key; omitting them looks up the agent
176
+ * created with no options.
177
+ */
178
+ export function getAgent(key, options = {}) {
179
+ return agentRegistry.get(agentCacheKey(normalizeRegistryKey(key), options));
161
180
  }
162
- export function hasAgent(key) {
163
- return agentRegistry.has(normalizeRegistryKey(key));
181
+ /**
182
+ * Whether an agent is registered for this key and TLS option set.
183
+ */
184
+ export function hasAgent(key, options = {}) {
185
+ return agentRegistry.has(agentCacheKey(normalizeRegistryKey(key), options));
164
186
  }
165
- export function removeAgent(key, destroy = true) {
187
+ /**
188
+ * Removes registered agents for a key.
189
+ *
190
+ * With `options` the single matching agent is removed; without them every
191
+ * agent registered for that host is removed, whatever TLS options it was
192
+ * created with, so a per-host teardown releases all of its sockets.
193
+ *
194
+ * @returns Whether anything was removed.
195
+ */
196
+ export function removeAgent(key, destroy = true, options) {
166
197
  const registryKey = normalizeRegistryKey(key);
167
- const agent = agentRegistry.get(registryKey);
168
- if (!agent) {
169
- return false;
170
- }
171
- agentRegistry.delete(registryKey);
172
- if (destroy) {
173
- agent.destroy();
198
+ const cacheKeys = options === undefined
199
+ ? Array.from(agentRegistry.keys()).filter((candidate) => candidate.startsWith(`${registryKey}|`))
200
+ : [agentCacheKey(registryKey, options)];
201
+ let removed = false;
202
+ for (const cacheKey of cacheKeys) {
203
+ const agent = agentRegistry.get(cacheKey);
204
+ if (!agent) {
205
+ continue;
206
+ }
207
+ agentRegistry.delete(cacheKey);
208
+ if (destroy) {
209
+ agent.destroy();
210
+ }
211
+ removed = true;
174
212
  }
175
- return true;
213
+ return removed;
176
214
  }
177
215
  export function clearAgents(destroy = true) {
178
216
  if (destroy) {
@@ -12,11 +12,17 @@ import type { CacheFreshness } from "./core/httpCacheControl.type.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 declare function calculateFreshness(responseHeaders: Readonly<Record<string, string>>, responseDate?: Date): CacheFreshness;
25
+ export declare function calculateFreshness(responseHeaders: Readonly<Record<string, string>>, responseDate?: Date, now?: Date): CacheFreshness;
20
26
  /**
21
27
  * Determines if a cached response is still fresh.
22
28
  */