@zudojs/http 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -0
- package/dist/httpAdapter/http.adapters.d.ts +24 -3
- package/dist/httpAdapter/http.adapters.js +20 -11
- package/dist/httpAdapter/node/httpNode.adapter.d.ts +2 -1
- package/dist/httpAdapter/node/httpNode.adapter.js +17 -2
- package/dist/httpAdapter/node/httpNode.request.js +7 -1
- package/dist/httpAdapter/node/httpNode.type.d.ts +14 -0
- package/dist/httpAgent/http.agent.d.ts +22 -3
- package/dist/httpAgent/http.agent.js +52 -14
- package/dist/httpCacheControl/httpCacheControl.freshness.d.ts +7 -1
- package/dist/httpCacheControl/httpCacheControl.freshness.js +30 -3
- package/dist/httpClient/httpClient.retry.d.ts +17 -12
- package/dist/httpClient/httpClient.retry.js +35 -10
- package/dist/httpClient/httpClient.type.d.ts +14 -0
- package/dist/httpErrors/httpError.base.js +2 -2
- package/dist/httpErrors/httpError.util.d.ts +8 -0
- package/dist/httpErrors/httpError.util.js +12 -0
- package/dist/httpFetchMount/httpFetchMount.core.d.ts +25 -0
- package/dist/httpFetchMount/httpFetchMount.core.js +84 -0
- package/dist/httpFetchMount/httpFetchMount.request.d.ts +21 -0
- package/dist/httpFetchMount/httpFetchMount.request.js +100 -0
- package/dist/httpFetchMount/httpFetchMount.type.d.ts +56 -0
- package/dist/httpFetchMount/httpFetchMount.type.js +5 -0
- package/dist/httpFetchMount/index.d.ts +11 -0
- package/dist/httpFetchMount/index.js +10 -0
- package/dist/httpKeepAlive/httpKeepAlive.core.js +14 -2
- package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.d.ts +18 -0
- package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.js +17 -1
- package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.d.ts +6 -3
- package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.js +34 -6
- package/dist/httpMiddleware/builtin/security/httpMiddleware.security.d.ts +11 -0
- package/dist/httpMiddleware/builtin/security/httpMiddleware.security.js +25 -12
- package/dist/httpMiddleware/httpMiddleware.type.d.ts +9 -1
- package/dist/httpMiddleware/pipeline/httpPipeline.execution.js +22 -45
- package/dist/httpMiddleware/pipeline/httpPipeline.guardResponse.d.ts +36 -0
- package/dist/httpMiddleware/pipeline/httpPipeline.guardResponse.js +57 -0
- package/dist/httpMiddleware/pipeline/httpPipeline.helper.d.ts +2 -1
- package/dist/httpMiddleware/pipeline/httpPipeline.helper.js +10 -0
- package/dist/httpMiddleware/pipeline/index.d.ts +1 -0
- package/dist/httpMiddleware/pipeline/index.js +1 -0
- package/dist/httpNegotiation/httpNegotiation.core.d.ts +13 -0
- package/dist/httpNegotiation/httpNegotiation.core.js +60 -8
- package/dist/httpOpenApi/httpOpenApi.document.d.ts +44 -0
- package/dist/httpOpenApi/httpOpenApi.document.js +61 -0
- package/dist/httpOpenApi/httpOpenApi.mount.d.ts +31 -0
- package/dist/httpOpenApi/httpOpenApi.mount.js +58 -0
- package/dist/httpOpenApi/httpOpenApi.type.d.ts +54 -0
- package/dist/httpOpenApi/httpOpenApi.type.js +5 -0
- package/dist/httpOpenApi/index.d.ts +13 -0
- package/dist/httpOpenApi/index.js +12 -0
- package/dist/httpOpenApi/routeTable/index.d.ts +11 -0
- package/dist/httpOpenApi/routeTable/index.js +11 -0
- package/dist/httpOpenApi/routeTable/routeTable.collect.d.ts +19 -0
- package/dist/httpOpenApi/routeTable/routeTable.collect.js +89 -0
- package/dist/httpOpenApi/routeTable/routeTable.merge.d.ts +15 -0
- package/dist/httpOpenApi/routeTable/routeTable.merge.js +37 -0
- package/dist/httpOpenApi/routeTable/routeTable.template.d.ts +30 -0
- package/dist/httpOpenApi/routeTable/routeTable.template.js +67 -0
- package/dist/httpProxy/http.proxy.d.ts +9 -0
- package/dist/httpProxy/http.proxy.js +35 -3
- package/dist/httpQuery/index.d.ts +11 -2
- package/dist/httpQuery/index.js +11 -2
- package/dist/httpQuery/queryParse/index.d.ts +10 -0
- package/dist/httpQuery/queryParse/index.js +10 -0
- package/dist/httpQuery/queryParse/queryParse.flat.d.ts +13 -0
- package/dist/httpQuery/queryParse/queryParse.flat.js +44 -0
- package/dist/httpQuery/queryParse/queryParse.nested.d.ts +25 -0
- package/dist/httpQuery/queryParse/queryParse.nested.js +112 -0
- package/dist/httpQuery/queryParse/queryParse.tokenizer.d.ts +37 -0
- package/dist/httpQuery/queryParse/queryParse.tokenizer.js +95 -0
- package/dist/httpQuery/queryRequest/index.d.ts +9 -0
- package/dist/httpQuery/queryRequest/index.js +9 -0
- package/dist/httpQuery/queryRequest/query.request.d.ts +43 -0
- package/dist/httpQuery/queryRequest/query.request.js +96 -0
- package/dist/httpQuery/querySerialize/index.d.ts +10 -0
- package/dist/httpQuery/querySerialize/index.js +10 -0
- package/dist/httpQuery/querySerialize/query.util.d.ts +21 -0
- package/dist/httpQuery/querySerialize/query.util.js +67 -0
- package/dist/httpQuery/querySerialize/querySerialize.core.d.ts +12 -0
- package/dist/httpQuery/querySerialize/querySerialize.core.js +97 -0
- package/dist/httpQuery/queryTypes/index.d.ts +11 -0
- package/dist/httpQuery/queryTypes/index.js +9 -0
- package/dist/httpQuery/queryTypes/query.container.d.ts +16 -0
- package/dist/httpQuery/queryTypes/query.container.js +51 -0
- package/dist/httpQuery/queryTypes/query.limit.d.ts +25 -0
- package/dist/httpQuery/queryTypes/query.limit.js +32 -0
- package/dist/httpQuery/queryTypes/query.type.d.ts +62 -0
- package/dist/httpQuery/queryTypes/query.type.js +2 -0
- package/dist/httpRedirect/http.redirect.d.ts +6 -0
- package/dist/httpRedirect/http.redirect.js +53 -2
- package/dist/httpRequest/http.request.d.ts +61 -2
- package/dist/httpRequest/http.request.js +86 -35
- package/dist/httpRequest/httpRequest.context.d.ts +8 -0
- package/dist/httpRequest/httpRequest.context.js +23 -18
- package/dist/httpRequest/index.d.ts +1 -0
- package/dist/httpRequest/index.js +1 -0
- package/dist/httpRequest/requestId/httpRequest.requestId.d.ts +25 -0
- package/dist/httpRequest/requestId/httpRequest.requestId.js +34 -0
- package/dist/httpRequest/requestId/index.d.ts +7 -0
- package/dist/httpRequest/requestId/index.js +7 -0
- package/dist/httpRequest/target/httpRequest.target.d.ts +2 -2
- package/dist/httpRequest/target/httpRequest.target.js +23 -5
- package/dist/httpResponse/httpResponse.writer.js +15 -0
- package/dist/httpRouter/core/factory/httpRoute.factory.base.d.ts +24 -4
- package/dist/httpRouter/core/factory/httpRoute.factory.base.js +73 -17
- package/dist/httpRouter/core/factory/httpRoute.factory.js +3 -3
- package/dist/httpRouter/core/group/httpRouterGroup.core.js +13 -0
- package/dist/httpRouter/core/register/httpRouter.register.js +19 -21
- package/dist/httpRouter/core/types/httpRouter.type.d.ts +37 -1
- package/dist/httpRouter/core/util/httpRoute.util.d.ts +55 -0
- package/dist/httpRouter/core/util/httpRoute.util.js +101 -4
- package/dist/httpRouter/dispatch/httpRoute.dispatcher.d.ts +11 -0
- package/dist/httpRouter/dispatch/httpRoute.dispatcher.js +25 -6
- package/dist/httpRouter/matching/httpRoute.matcher.core.js +51 -6
- package/dist/httpRouter/matching/httpRoute.matcher.d.ts +0 -1
- package/dist/httpRouter/matching/httpRoute.matcher.js +64 -26
- package/dist/httpRouter/pattern/httpRoute.pattern.parse.d.ts +16 -0
- package/dist/httpRouter/pattern/httpRoute.pattern.parse.js +40 -11
- package/dist/httpRouter/pattern/index.d.ts +1 -1
- package/dist/httpRouter/pattern/index.js +1 -1
- package/dist/httpSecurity/httpSecurity.config.js +4 -1
- package/dist/httpSecurity/httpSecurity.validator.js +16 -7
- package/dist/httpServer/factory/httpServer.factory.d.ts +10 -9
- package/dist/httpServer/factory/httpServer.factory.js +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +11 -8
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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/
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 =
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
agent
|
|
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
|
|
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
|
*/
|