@fluojs/http 1.1.2 → 3.0.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 (125) hide show
  1. package/README.ko.md +430 -15
  2. package/README.md +437 -15
  3. package/dist/access-log-observer.d.ts +77 -0
  4. package/dist/access-log-observer.d.ts.map +1 -0
  5. package/dist/access-log-observer.js +171 -0
  6. package/dist/adapter.d.ts +9 -0
  7. package/dist/adapter.d.ts.map +1 -1
  8. package/dist/adapter.js +15 -1
  9. package/dist/adapters/binding.d.ts.map +1 -1
  10. package/dist/adapters/binding.js +18 -16
  11. package/dist/adapters/dto-binding-plan.d.ts +2 -2
  12. package/dist/adapters/dto-binding-plan.d.ts.map +1 -1
  13. package/dist/adapters/dto-binding-plan.js +13 -4
  14. package/dist/adapters/dto-validation-adapter.d.ts +2 -1
  15. package/dist/adapters/dto-validation-adapter.d.ts.map +1 -1
  16. package/dist/adapters/dto-validation-adapter.js +2 -2
  17. package/dist/byte-range-response.d.ts +71 -0
  18. package/dist/byte-range-response.d.ts.map +1 -0
  19. package/dist/byte-range-response.js +347 -0
  20. package/dist/client-identity.d.ts +8 -4
  21. package/dist/client-identity.d.ts.map +1 -1
  22. package/dist/client-identity.js +12 -93
  23. package/dist/compiled-route-identity.d.ts +17 -0
  24. package/dist/compiled-route-identity.d.ts.map +1 -0
  25. package/dist/compiled-route-identity.js +29 -0
  26. package/dist/connection.d.ts +67 -0
  27. package/dist/connection.d.ts.map +1 -0
  28. package/dist/connection.js +480 -0
  29. package/dist/context/request-context-node-store.d.ts +6 -0
  30. package/dist/context/request-context-node-store.d.ts.map +1 -1
  31. package/dist/context/request-context-node-store.js +14 -0
  32. package/dist/context/request-context.d.ts +5 -5
  33. package/dist/context/request-context.d.ts.map +1 -1
  34. package/dist/context/request-context.js +8 -83
  35. package/dist/context/request-id.d.ts +10 -0
  36. package/dist/context/request-id.d.ts.map +1 -0
  37. package/dist/context/request-id.js +27 -0
  38. package/dist/context/sse.d.ts +13 -0
  39. package/dist/context/sse.d.ts.map +1 -1
  40. package/dist/context/sse.js +36 -6
  41. package/dist/cookie-helpers.d.ts +47 -0
  42. package/dist/cookie-helpers.d.ts.map +1 -0
  43. package/dist/cookie-helpers.js +166 -0
  44. package/dist/decorators.d.ts +52 -19
  45. package/dist/decorators.d.ts.map +1 -1
  46. package/dist/decorators.js +57 -10
  47. package/dist/dispatch/conditional-request-policy.d.ts +67 -0
  48. package/dist/dispatch/conditional-request-policy.d.ts.map +1 -0
  49. package/dist/dispatch/conditional-request-policy.js +297 -0
  50. package/dist/dispatch/dispatch-content-negotiation.d.ts +8 -0
  51. package/dist/dispatch/dispatch-content-negotiation.d.ts.map +1 -1
  52. package/dist/dispatch/dispatch-content-negotiation.js +89 -42
  53. package/dist/dispatch/dispatch-error-negotiation.d.ts +26 -0
  54. package/dist/dispatch/dispatch-error-negotiation.d.ts.map +1 -0
  55. package/dist/dispatch/dispatch-error-negotiation.js +110 -0
  56. package/dist/dispatch/dispatch-error-policy.d.ts +1 -10
  57. package/dist/dispatch/dispatch-error-policy.d.ts.map +1 -1
  58. package/dist/dispatch/dispatch-error-policy.js +1 -33
  59. package/dist/dispatch/dispatch-error-representation.d.ts +18 -0
  60. package/dist/dispatch/dispatch-error-representation.d.ts.map +1 -0
  61. package/dist/dispatch/dispatch-error-representation.js +125 -0
  62. package/dist/dispatch/dispatch-response-metadata.d.ts +10 -0
  63. package/dist/dispatch/dispatch-response-metadata.d.ts.map +1 -0
  64. package/dist/dispatch/dispatch-response-metadata.js +12 -0
  65. package/dist/dispatch/dispatch-response-policy.d.ts +33 -5
  66. package/dist/dispatch/dispatch-response-policy.d.ts.map +1 -1
  67. package/dist/dispatch/dispatch-response-policy.js +149 -18
  68. package/dist/dispatch/dispatcher.d.ts +5 -1
  69. package/dist/dispatch/dispatcher.d.ts.map +1 -1
  70. package/dist/dispatch/dispatcher.js +178 -107
  71. package/dist/dispatch/fast-path/debug-visibility.d.ts.map +1 -1
  72. package/dist/dispatch/fast-path/debug-visibility.js +6 -3
  73. package/dist/dispatch/fast-path/dispatcher-state.d.ts +10 -0
  74. package/dist/dispatch/fast-path/dispatcher-state.d.ts.map +1 -0
  75. package/dist/dispatch/fast-path/dispatcher-state.js +52 -0
  76. package/dist/dispatch/fast-path/eligibility-checker.d.ts.map +1 -1
  77. package/dist/dispatch/fast-path/eligibility-checker.js +27 -15
  78. package/dist/dispatch/fast-path/eligibility.d.ts +16 -10
  79. package/dist/dispatch/fast-path/eligibility.d.ts.map +1 -1
  80. package/dist/dispatch/fast-path/eligibility.js +10 -4
  81. package/dist/dispatch/fast-path/fast-path-executor.d.ts +14 -1
  82. package/dist/dispatch/fast-path/fast-path-executor.d.ts.map +1 -1
  83. package/dist/dispatch/fast-path/fast-path-executor.js +23 -6
  84. package/dist/dispatch/request-abort.d.ts +22 -0
  85. package/dist/dispatch/request-abort.d.ts.map +1 -0
  86. package/dist/dispatch/request-abort.js +30 -0
  87. package/dist/dispatch/response-integration.d.ts +55 -0
  88. package/dist/dispatch/response-integration.d.ts.map +1 -0
  89. package/dist/dispatch/response-integration.js +57 -0
  90. package/dist/errors.d.ts +10 -0
  91. package/dist/errors.d.ts.map +1 -1
  92. package/dist/errors.js +19 -0
  93. package/dist/guards.d.ts +1 -1
  94. package/dist/guards.d.ts.map +1 -1
  95. package/dist/header-helpers.d.ts +59 -0
  96. package/dist/header-helpers.d.ts.map +1 -0
  97. package/dist/header-helpers.js +185 -0
  98. package/dist/index.d.ts +1 -16
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +4 -15
  101. package/dist/index.portable.d.ts +25 -0
  102. package/dist/index.portable.d.ts.map +1 -0
  103. package/dist/index.portable.js +21 -0
  104. package/dist/internal.d.ts +3 -1
  105. package/dist/internal.d.ts.map +1 -1
  106. package/dist/internal.js +2 -0
  107. package/dist/mapping.d.ts.map +1 -1
  108. package/dist/mapping.js +100 -15
  109. package/dist/middleware/correlation.d.ts +9 -1
  110. package/dist/middleware/correlation.d.ts.map +1 -1
  111. package/dist/middleware/correlation.js +25 -14
  112. package/dist/middleware/cors.d.ts.map +1 -1
  113. package/dist/middleware/cors.js +7 -8
  114. package/dist/middleware/middleware.d.ts +3 -3
  115. package/dist/middleware/middleware.d.ts.map +1 -1
  116. package/dist/middleware/rate-limit.d.ts +13 -2
  117. package/dist/middleware/rate-limit.d.ts.map +1 -1
  118. package/dist/middleware/rate-limit.js +1 -0
  119. package/dist/static-assets.d.ts +77 -0
  120. package/dist/static-assets.d.ts.map +1 -0
  121. package/dist/static-assets.js +270 -0
  122. package/dist/types.d.ts +225 -17
  123. package/dist/types.d.ts.map +1 -1
  124. package/dist/types.js +71 -1
  125. package/package.json +19 -9
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  <p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
4
4
 
5
+ Node.js support is `>=24.0.0 <27`. See [Node.js support and migration](../../docs/reference/node-support.md) before upgrading.
6
+
5
7
  The HTTP execution layer that turns route metadata into a request pipeline with binding, validation, guards, interceptors, and response writing.
6
8
 
7
9
  ## Table of Contents
@@ -10,6 +12,12 @@ The HTTP execution layer that turns route metadata into a request pipeline with
10
12
  - [When to Use](#when-to-use)
11
13
  - [Quick Start](#quick-start)
12
14
  - [Common Patterns](#common-patterns)
15
+ - [Static Asset Delivery](#static-asset-delivery)
16
+ - [Response Cookies](#response-cookies)
17
+ - [Early Hints](#early-hints)
18
+ - [Realtime Adapter Capabilities](#realtime-adapter-capabilities)
19
+ - [Byte Range Responses](#byte-range-responses)
20
+ - [HTTP Error Representations](#http-error-representations)
13
21
  - [Request Cleanup and Portability](#request-cleanup-and-portability)
14
22
  - [Public API](#public-api)
15
23
  - [Related Packages](#related-packages)
@@ -21,6 +29,29 @@ The HTTP execution layer that turns route metadata into a request pipeline with
21
29
  npm install @fluojs/http
22
30
  ```
23
31
 
32
+ ## Static Asset Delivery
33
+
34
+ `createStaticAssetsMiddleware(...)` serves only `GET` and `HEAD` requests through an explicit application-owned `StaticAssetSource`. The portable HTTP package never assumes filesystem access, so Web and edge applications must provide their own source instead of receiving an implicit Node fallback.
35
+
36
+ The middleware decodes each URL segment once and rejects traversal, encoded separators, backslashes, and NUL before it asks the source to resolve anything. Dotfiles use an explicit policy: `allow` resolves them, `ignore` leaves the request to later middleware/routes, and `deny` commits `403`, including a configured dotfile index. Directory indexes are disabled by default and are considered only for a trailing-slash URL.
37
+
38
+ ```ts
39
+ import { createStaticAssetsMiddleware } from '@fluojs/http';
40
+ import { createNodeFileSystemAssetSource } from '@fluojs/platform-nodejs';
41
+
42
+ const assets = createStaticAssetsMiddleware({
43
+ cacheControl: 'public, max-age=3600',
44
+ index: ['index.html'],
45
+ prefix: '/assets',
46
+ source: createNodeFileSystemAssetSource({
47
+ precompressed: true,
48
+ root: './public',
49
+ }),
50
+ });
51
+ ```
52
+
53
+ Register `assets` in runtime bootstrap `middleware`. The selected representation owns MIME type, exact bytes and length, `ETag`, `Last-Modified`, and optional `Content-Encoding`; static writes bypass adapter dynamic compression so those values remain coherent for full `GET`, `HEAD`, conditional fields, `Range`, and `If-Range`. A source selects only request-acceptable `br`, `gzip`, or identity bytes, returns an explicit no-representation outcome for bodyless `406`, and uses `Vary: Accept-Encoding` whenever selection can vary. Byte ranges address the selected encoded representation.
54
+
24
55
  ## When to Use
25
56
 
26
57
  Use this package when you need to:
@@ -40,12 +71,12 @@ class CreateUserDto {
40
71
  @FromBody()
41
72
  @IsString()
42
73
  @MinLength(3)
43
- name!: string;
74
+ name = '';
44
75
  }
45
76
 
46
77
  class FindUserParamsDto {
47
78
  @FromPath('id')
48
- id!: string;
79
+ id = '';
49
80
  }
50
81
 
51
82
  @Controller('/users')
@@ -64,6 +95,23 @@ export class UserController {
64
95
  }
65
96
  ```
66
97
 
98
+ Initialize decorated DTO fields, as shown above, or declare them optional. A definite assignment assertion such as `name!: string` does not compile with the Babel decorator configuration Fluo ships, which rejects a definitely assigned field on a decorated class with `Definitely assigned fields cannot be initialized here, but only in the constructor`.
99
+
100
+ ### Optional route paths
101
+
102
+ `Get`, `Post`, `Put`, `Patch`, `Delete`, `Options`, `Head`, `All`, `Sse`, and HTTP `Query`
103
+ accept omitted or `undefined` paths, meaning `''`. `Route(method)` has the same path default,
104
+ but its method remains required. With `@Controller('cats')`, `@Get()` dispatches `GET /cats`;
105
+ with `@Controller()`, it dispatches `GET /`. `('/')` resolves to the same route but keeps a
106
+ different raw path; it never escapes the controller prefix. Duplicate detection and invalid
107
+ path/method rejection are unchanged. These are factories, not bare `@Get` decorators.
108
+
109
+ `Sse()` preserves GET plus `text/event-stream` and stream lifecycle. `All()` remains a wildcard
110
+ and HTTP `Query()` means RFC QUERY, not query-string binding or GraphQL Query. Root and
111
+ `@fluojs/http/portable` share these defaults without expanding adapter method support. See the
112
+ [complete 165-API reconciliation](../../docs/reference/decorator-defaults.md) for unchanged
113
+ required arguments such as `HttpCode(status)` and `Header(name, value)`.
114
+
67
115
  ### Route path contract
68
116
 
69
117
  HTTP route decorators such as `@Controller()`, `@Get()`, and `@Post()` accept only:
@@ -75,6 +123,125 @@ Trailing slashes and duplicate slashes are normalized during route mapping, so `
75
123
 
76
124
  Route decorators do **not** support wildcard, regex-like, or mixed-segment syntax such as `*`, `?`, `/(.*)`, `user-:id`, or `:id.json`. Wildcard matching remains middleware-only via `forRoutes('/users/*')`.
77
125
 
126
+ Catch-all route grammar is intentionally deferred. The
127
+ [HTTP catch-all route grammar decision](../../docs/architecture/http-catch-all-route-grammar.md)
128
+ records the evaluated syntaxes, provisional precedence and params shape, OpenAPI limitations,
129
+ adapter native fast-path constraints, and the evidence required before this HTTP contract can be
130
+ revisited. No syntax described there is active route behavior.
131
+
132
+ ### Custom HTTP method contract
133
+
134
+ Use `@Query(path)` for RFC `QUERY`, or `@Route(method, path)` for another HTTP extension method such as `PURGE` or WebDAV `PROPFIND`:
135
+
136
+ ```ts
137
+ import { Controller, Query, Route } from '@fluojs/http';
138
+
139
+ @Controller('/operations')
140
+ export class OperationsController {
141
+ @Query('/search')
142
+ search() {
143
+ return { method: 'QUERY' };
144
+ }
145
+
146
+ @Route('purge', '/cache')
147
+ purgeCache() {
148
+ return { method: 'PURGE' };
149
+ }
150
+ }
151
+ ```
152
+
153
+ `@Route(...)` accepts a non-empty HTTP token, canonicalizes it to uppercase before metadata registration, and rejects whitespace, separators, control characters, and non-ASCII token characters with `InvalidHttpMethodError`. `ALL` is reserved for the framework-owned `@All(...)` wildcard and is rejected by `@Route(...)`. Method-specific routes, including custom methods, take precedence over `@All(...)`, participate in duplicate detection and route versioning, and use the ordinary DTO binding, validation, guard, interceptor, and response pipeline. Unless status metadata says otherwise, successful `QUERY` and extension-method handlers default to `200`.
154
+
155
+ Adapter wire support is an explicit portability contract. Supported Node listeners, Fastify and Express wildcard fallbacks, and Bun, Deno, and Cloudflare Workers fetch dispatch execute `QUERY` and representative extension methods without converting them into ordinary methods. Custom methods stay off Bun native `routes` acceleration, while Fastify registers their method names only so its wildcard fallback can receive them; neither path creates a native fluo route handoff. `CONNECT` remains outside ordinary controller-route conformance.
156
+
157
+ Custom runtime methods do not become OpenAPI Path Item operations automatically. `@fluojs/openapi` continues to accept only its documented standard operation methods, so exclude custom-method descriptors from OpenAPI input or document those endpoints through an application-owned extension.
158
+
159
+ ### Portable header helpers
160
+
161
+ Use `getRequestHeader(request, name)` when middleware, versioning, DTO binding, or controller code
162
+ needs a case-insensitive lookup without flattening adapter-provided `string | string[] | undefined`
163
+ header values.
164
+
165
+ Use `appendVaryHeader(response, ...fields)` when response negotiation or caching logic needs to add
166
+ `Vary` fields without duplicating case variants, re-parsing comma lists by hand, or accidentally
167
+ expanding an existing wildcard `Vary: *` contract.
168
+
169
+ Use `getResponseHeader(response, name)` and `hasResponseHeader(response, name)` for the same
170
+ case-insensitive lookup over adapter-provided response headers. They preserve the original
171
+ `string | string[]` shape and do not write headers, body, status, or commit state.
172
+
173
+ Use `buildContentDisposition(disposition, filename)` to create an `attachment` or `inline`
174
+ Content-Disposition field value. It emits an escaped printable-ASCII `filename` fallback and a
175
+ deterministic RFC 8187 UTF-8 `filename*` value. Carriage return and line feed filenames reject
176
+ before a header value is returned.
177
+
178
+ ```ts
179
+ import {
180
+ appendVaryHeader,
181
+ buildContentDisposition,
182
+ getRequestHeader,
183
+ getResponseHeader,
184
+ hasResponseHeader,
185
+ type RequestContext,
186
+ } from '@fluojs/http';
187
+
188
+ export function readLanguage(context: RequestContext): string | undefined {
189
+ const acceptLanguage = getRequestHeader(context.request, 'accept-language');
190
+ return Array.isArray(acceptLanguage) ? acceptLanguage[0] : acceptLanguage;
191
+ }
192
+
193
+ export function markLanguageVariance(context: RequestContext): void {
194
+ appendVaryHeader(context.response, 'Accept-Language', 'Origin');
195
+ context.response.setHeader(
196
+ 'Content-Disposition',
197
+ buildContentDisposition('attachment', 'résumé.pdf'),
198
+ );
199
+ }
200
+
201
+ export function readResponseEtag(
202
+ context: RequestContext,
203
+ ): string | string[] | undefined {
204
+ return getResponseHeader(context.response, 'etag');
205
+ }
206
+
207
+ export function shouldSetResponseEtag(context: RequestContext): boolean {
208
+ return !hasResponseHeader(context.response, 'etag');
209
+ }
210
+ ```
211
+
212
+ ### Content negotiation
213
+
214
+ Configure `ContentNegotiationOptions` with response formatters, then use `@Produces(...)` to
215
+ limit each route to the representations it can return. The dispatcher owns formatter selection,
216
+ response commit, `Content-Type`, canonical 406 responses, and `Vary: Accept`; handlers only return
217
+ their values.
218
+
219
+ ```ts
220
+ import { Controller, Get, Produces } from '@fluojs/http';
221
+
222
+ @Controller('/reports')
223
+ export class ReportController {
224
+ @Produces('application/json', 'text/plain')
225
+ @Get('/')
226
+ getReport() {
227
+ return { ok: true };
228
+ }
229
+ }
230
+ ```
231
+
232
+ The configured default formatter is used when `Accept` is absent, blank, or `*/*`; if that
233
+ formatter is not allowed by `@Produces(...)`, the first declared allowed formatter is used instead.
234
+ For a supplied header, exact ranges, `type/*`, `*/*`, and structured suffix ranges such as
235
+ `application/*+json` are matched case-insensitively. Higher `q` wins, then the more specific range,
236
+ then the configured default (or formatter declaration order). A more-specific `q=0` range excludes
237
+ that representation even when a broader wildcard is positive.
238
+
239
+ Each `q` value must be between `0` and `1` with at most three fractional digits. Malformed media
240
+ ranges or qualities are ignored; if no valid acceptable representation remains, or no formatter is
241
+ allowed or matched, the dispatcher returns its canonical `406 Not Acceptable` response. Every
242
+ successful formatter selection emits one canonical, deduplicated `Vary: Accept` while preserving
243
+ any existing `Vary` fields.
244
+
78
245
  ## Common Patterns
79
246
 
80
247
  ### Guards and interceptors
@@ -93,6 +260,37 @@ class AdminController {
93
260
  }
94
261
  ```
95
262
 
263
+ ### Request observers
264
+
265
+ `onRequestSuccess` runs only after the matched handler and all module-level and application-level middleware have settled, including work after `await next()`. If middleware throws after `next()` returns, observers receive `onRequestError` without a preceding success notification. `onRequestFinish` still runs after either outcome.
266
+
267
+ ### Access logging
268
+
269
+ `createAccessLogObserver(...)` turns the request-observer lifecycle into application-owned structured records. It emits a start record, an error record for each dispatch error, and exactly one terminal finish record with a monotonic duration, optional request ID, method, path, matched route, status, and outcome (`success`, `handled_error`, `unhandled_error`, `not_found`, or `aborted`). Native route dispatch falls back to this complete lifecycle when observers are configured.
270
+
271
+ The sink is deliberately consumer-owned: route `AccessLogEvent` values to the structured logger, telemetry pipeline, or retention policy that owns your operational data. No headers are emitted unless they are allowlisted. `authorization`, `cookie`, `set-cookie`, `proxy-authorization`, and `x-api-key` remain redacted even when allowlisted; add organization-specific names with `redact`.
272
+
273
+ ```ts
274
+ import { createAccessLogObserver } from '@fluojs/http';
275
+
276
+ const accessLogObserver = createAccessLogObserver({
277
+ clientIdentity: {},
278
+ headers: {
279
+ allow: ['user-agent', 'set-cookie'],
280
+ redact: ['x-tenant-token'],
281
+ },
282
+ sink: {
283
+ emit(event) {
284
+ structuredLog.write(event);
285
+ },
286
+ },
287
+ });
288
+ ```
289
+
290
+ Omit `clientIdentity` when no client address is needed. `clientIdentity: {}` explicitly opts into the adapter's direct transport peer and ignores forwarding fields. Use `clientIdentity: { trustProxy: ['10.0.0.0/8'] }` only when a trusted proxy boundary should supply forwarded identity.
291
+
292
+ Request IDs are optional. An observer alone does not create one, so observer-only records can omit `requestId`. When `createCorrelationMiddleware()` is installed, the dispatcher adopts an incoming `x-request-id` or legacy `x-correlation-id`, or generates an ID before the access-log start record.
293
+
96
294
  ### Async request context
97
295
 
98
296
  ```ts
@@ -104,11 +302,136 @@ function someDeepHelper() {
104
302
  }
105
303
  ```
106
304
 
107
- `runWithRequestContext(...)` preserves the active context across awaited work when the host provides `AsyncLocalStorage` through `globalThis.AsyncLocalStorage` or Node's built-in `node:async_hooks` module. The root `@fluojs/http` import does not probe or instantiate async-context storage; helpers resolve storage lazily on first use, guard `process.getBuiltinModule(...)` failures, and keep the first-call synchronous callback return and throw behavior unchanged while isolating promise continuations registered before Node async storage finishes resolving. Non-Node hosts without an async-context primitive use a synchronous stack fallback that clears the context before awaited continuations resume, avoiding cross-request leaks instead of pretending to isolate overlapping async work.
305
+ `runWithRequestContext(...)` preserves the active context across awaited work when the host provides `AsyncLocalStorage` through `globalThis.AsyncLocalStorage` or the `node:async_hooks` module. The root `@fluojs/http` export selects a runtime-specific entrypoint without probing or instantiating async-context storage: Node and Bun register the host constructor during module initialization, while Deno, worker, browser, and default entries remain free of Node built-in imports. The request-local store itself is still created lazily on first use. Promise-returning non-async callbacks keep synchronous invocation, return, and throw behavior, and their continuations retain the bound context until the returned promise settles. The helpers never replace `Promise.prototype.then`, so unrelated promise continuations cannot capture a request. Hosts without an async-context primitive use a synchronous-only fallback that clears the context before awaited work resumes.
306
+
307
+ ## Response Cookies
308
+
309
+ Use the portable `setCookie()` and `clearCookie()` helpers instead of adapter-native response APIs. Every call writes one independent `Set-Cookie` field, so repeated calls preserve their order and are never comma-folded.
310
+
311
+ ```ts
312
+ import { clearCookie, setCookie } from '@fluojs/http';
313
+
314
+ setCookie(context.response, 'session', sessionToken, {
315
+ httpOnly: true,
316
+ maxAgeSeconds: 60 * 60,
317
+ path: '/',
318
+ sameSite: 'lax',
319
+ secure: true,
320
+ });
321
+
322
+ clearCookie(context.response, 'session', {
323
+ path: '/',
324
+ });
325
+ ```
326
+
327
+ `maxAgeSeconds` is a non-negative whole-second lifetime on every adapter. Values are percent-encoded, names and attributes are validated before the response changes, and `sameSite: 'none'` requires `secure: true`. To delete the same browser cookie, repeat its original `path` and `domain`; `httpOnly`, `secure`, and `sameSite` are policy attributes rather than browser matching keys.
328
+
329
+ ## Early Hints
330
+
331
+ `FrameworkResponse.earlyHints` is an optional, request-scoped capability for HTTP `103` informational responses. Check for property presence before use; property absence means the active adapter cannot emit Early Hints. There is no required `FrameworkResponse.writeEarlyHints()` method and unsupported adapters never silently ignore a write.
332
+
333
+ ```ts
334
+ import type { RequestContext } from '@fluojs/http';
335
+
336
+ async function render(_input: undefined, context: RequestContext) {
337
+ const earlyHints = context.response.earlyHints;
338
+
339
+ if (earlyHints) {
340
+ await earlyHints.write({
341
+ link: [
342
+ '</styles.css>; rel=preload; as=style',
343
+ '</app.js>; rel=modulepreload',
344
+ ],
345
+ 'x-trace-id': 'render-1',
346
+ });
347
+ }
348
+
349
+ context.response.setHeader('link', '</final.css>; rel=stylesheet');
350
+ return { ok: true };
351
+ }
352
+ ```
353
+
354
+ Each `write(...)` emits one `103`, so callers may await multiple writes before the final response. Every write requires at least one non-empty `link` value and may include additional informational fields with valid HTTP names and values. Header names are case-insensitive and cannot be repeated with different casing. Status-forbidden framing fields (`content-length` and `transfer-encoding`) are rejected before the native write. Early fields do not populate `response.headers`, change status, set `committed`, or become final-response headers.
355
+
356
+ Node.js, Express, and Fastify expose this capability. Fetch-style Web, Bun, Deno, and Cloudflare Workers responses omit it because their `Response` APIs cannot represent an informational response before the final response. A write after final commitment or a native validation/write failure rejects with `EarlyHintsWriteError` (`EARLY_HINTS_WRITE_FAILED`); a disconnect before settlement rejects with `RequestAbortedError` (`REQUEST_ABORTED`).
357
+
358
+ ## Realtime Adapter Capabilities
359
+
360
+ `HttpApplicationAdapter.getRealtimeCapability()` reports whether a platform is server-backed, fetch-style, or unsupported for realtime protocol integration. The fetch-style capability remains version 1. Hosts may additionally expose its optional, independently versioned `bindingInstallation` extension so first-party realtime packages can install their binding before adapter `listen()` starts without changing the stable capability discriminator.
361
+
362
+ `createFetchStyleHttpAdapterRealtimeCapability(reason, options)` always returns the source-compatible version 1 capability. When an installer is supplied, the returned value also includes `bindingInstallation`; that installer accepts a protocol-owned binding or `undefined` for pre-listen cleanup. The platform adapter remains responsible for parsing that boundary into its host-specific binding type. Once a managed adapter is live, its `close()` boundary owns final binding cleanup. Application code should normally register `@fluojs/websockets` or `@fluojs/socket.io` modules rather than call this low-level adapter capability directly.
363
+
364
+ ## HTTP Error Representations
365
+
366
+ Canonical JSON remains the default error response. Register an optional application-owned HTML
367
+ provider at runtime bootstrap when browser requests should receive complete error or not-found
368
+ documents without changing API clients:
369
+
370
+ ```ts
371
+ import type { HttpErrorRepresentationOptions } from '@fluojs/http';
372
+ import { bootstrapApplication } from '@fluojs/runtime';
373
+
374
+ function escapeHtml(value: string): string {
375
+ return value
376
+ .replaceAll('&', '&amp;')
377
+ .replaceAll('<', '&lt;')
378
+ .replaceAll('>', '&gt;')
379
+ .replaceAll('"', '&quot;')
380
+ .replaceAll("'", '&#39;');
381
+ }
382
+
383
+ const errorRepresentation = {
384
+ html: {
385
+ canRender({ request }) {
386
+ return request.method === 'GET' || request.method === 'HEAD';
387
+ },
388
+ render({ json }) {
389
+ return `<!doctype html><main>${json.error.status}: ${escapeHtml(json.error.message)}</main>`;
390
+ },
391
+ },
392
+ } satisfies HttpErrorRepresentationOptions;
393
+
394
+ const app = await bootstrapApplication({
395
+ errorRepresentation,
396
+ rootModule: AppModule,
397
+ });
398
+ ```
399
+
400
+ HTTP classifies the outcome before representation selection. Route misses become the existing 404
401
+ outcome, and uncommitted `HttpException` values from middleware, DTO binding/validation, guards,
402
+ interceptors, and handlers use the same seam. The provider receives the classified exception,
403
+ canonical `ErrorResponse`, request, optional matched handler, request id, and active request-scope
404
+ container. It receives no `FrameworkResponse`, so status, headers, `HEAD`, abort, and commit ownership
405
+ remain in the dispatcher.
406
+
407
+ The provider return value is trusted application HTML. fluo does not escape or sanitize it. Escape
408
+ every request-derived or error-derived value before interpolation, as the example does for
409
+ `json.error.message`, or render through a framework whose text-node contract performs that escaping.
410
+
411
+ `Accept` negotiation is deterministic: absent `Accept` and wildcard/tie cases select JSON; quality
412
+ and specificity select between `application/json` and available `text/html`; unsupported ranges
413
+ produce canonical JSON 406. `canRender(...)` may constrain HTML per application or matched handler.
414
+ A provider failure falls back once to the original canonical JSON outcome, and committed or aborted
415
+ requests are never rewritten. Response writer `send(...)` or stream/write failures propagate
416
+ unchanged and do not trigger a second canonical JSON write. Existing native `Vary` values are
417
+ preserved when HTTP adds `Accept`. Successful-route `@Produces(...)` metadata does not control error
418
+ representations. See the
419
+ [HTTP error representation decision](../../docs/architecture/http-error-representations.md) for the
420
+ complete phase and fallback contract.
108
421
 
109
422
  ### Rate limiting behind proxies
110
423
 
111
- `createRateLimitMiddleware(...)` resolves client identity from the raw socket `remoteAddress` by default. To trust `Forwarded`, `X-Forwarded-For`, or `X-Real-IP`, opt in with `trustProxyHeaders: true` only when your adapter sits behind a trusted proxy that overwrites those headers. If your adapter exposes neither a trusted proxy chain nor a raw socket identity, provide an explicit `keyResolver`.
424
+ `createRateLimitMiddleware(...)` resolves client identity from the adapter-snapshotted direct transport address by default. To trust `Forwarded`, `X-Forwarded-For`, or `X-Real-IP`, configure `trustProxy` with an explicit hop count, address/CIDR list, or predicate. Forwarded data is ignored unless the direct peer satisfies that policy; malformed `Forwarded` data fails closed to the direct transport identity.
425
+
426
+ ```ts
427
+ import { resolveHttpConnection } from '@fluojs/http';
428
+
429
+ const connection = resolveHttpConnection(context.request, {
430
+ trustProxy: ['10.0.0.0/8', '2001:db8:feed::/48'],
431
+ });
432
+ ```
433
+
434
+ `connection` is immutable and exposes the selected `clientAddress`, direct `remoteAddress`, trusted `proxyChain`, `protocol`, `secure`, `host`, `hostname`, and `port`. Fetch-only adapters may leave the direct address undefined because the Web `Request` contract does not expose it. A fetch-style HTTPS `Request` without an adapter-provided `connection` snapshot or explicit headers has no peer, host, or port, and `resolveHttpConnection(...)` does not infer HTTPS, `secure`, host, or port from its URL. The legacy `trustProxyHeaders: true` setting is broad compatibility only and is not recommended for new deployments; use `trustProxy` to describe the deployment boundary precisely. Only use either setting when you control the proxy that rewrites those headers. If an adapter provides neither a trusted proxy chain nor a raw socket identity, provide an explicit `keyResolver`.
112
435
 
113
436
  ### Server-sent events
114
437
 
@@ -129,7 +452,9 @@ export class OrdersEventsController {
129
452
  }
130
453
  ```
131
454
 
132
- `@Sse(path)` registers a `GET` route and declares `text/event-stream` produced media type metadata. Handlers may either return `SseResponse` for manual stream control or return `AsyncIterable<SseMessage<T> | T>` for managed streaming. Managed async iterables are converted with the same `encodeSseMessage(...)` behavior as `SseResponse`: plain yielded values become `data:` frames, while yielded objects with a `data` field may also provide `event`, `id`, and `retry`. The dispatcher stops consuming the source when `RequestContext.request.signal` aborts or the response stream closes, calls `FrameworkResponseStream.waitForDrain()` when a write reports backpressure, closes the stream on completion or source errors, and routes thrown source errors through the normal dispatcher error/observer seam after the already-committed SSE response is closed. Observable values remain out of scope and no RxJS dependency is required.
455
+ `@Sse(path)` registers a `GET` route and declares `text/event-stream` produced media type metadata. Handlers may either return `SseResponse` for manual stream control or return `AsyncIterable<SseMessage<T> | T>` for managed streaming. A manual `SseResponse` keeps its dispatch, request observers, and request-scoped resources active until explicit close, request abort, or raw stream close; those lifecycle stages then release exactly once. Managed async iterables are converted with the same `encodeSseMessage(...)` behavior as `SseResponse`: plain yielded values become `data:` frames, while yielded objects with a `data` field may also provide `event`, `id`, and `retry`. The dispatcher stops consuming the source when `RequestContext.request.signal` aborts or the response stream closes, calls `FrameworkResponseStream.waitForDrain()` when a write reports backpressure, and closes the stream on completion or source errors. The same cancellation boundary bounds an in-flight `waitForDrain()`: request abort or stream close wins over an unsettled drain promise, after which the dispatcher closes the source iterator exactly once and continues request-scope disposal. Stream write failures and rejected drain promises still propagate their original errors. On cancellation, the dispatcher closes the response stream promptly and awaits the source iterator's `return()` cleanup before disposing request-scoped resources. Cleanup failures are reported through the request observer and dispatcher logger seams without replacing the already-committed SSE response. Thrown source errors follow the same committed-response error/observer boundary. Observable values remain out of scope and no RxJS dependency is required.
456
+
457
+ Managed SSE requires an adapter that exposes `FrameworkResponse.stream`. When the active adapter does not provide a response stream, the dispatcher rejects the managed async iterable before marking the response handled and surfaces the failure through the standard dispatch error path (request error observers and the configured error response writer) instead of silently reporting the stream as handled.
133
458
 
134
459
  On the browser side, create the `EventSource` inside the React effect that owns it and always close it from the cleanup function so route changes, Strict Mode remounts, and component unmounts do not leave duplicate streams open:
135
460
 
@@ -176,7 +501,7 @@ Use `runWithRequestContext(...)`, `assertRequestContext()`, `createRequestContex
176
501
 
177
502
  ### Fast-path observability
178
503
 
179
- The dispatcher exposes fast-path observability for adapters and diagnostics through `FAST_PATH_ELIGIBILITY_SYMBOL`, `FAST_PATH_STATS_SYMBOL`, `formatFastPathStats(...)`, and `getDispatcherFastPathStats(...)`.
504
+ The dispatcher exposes fast-path observability for adapters and diagnostics through `FAST_PATH_ELIGIBILITY_SYMBOL`, `FAST_PATH_STATS_SYMBOL`, `formatFastPathStats(...)`, and `getDispatcherFastPathStats(...)`. Eligibility decisions belong to the dispatcher instance rather than the shared `HandlerMapping`: dispatchers may reuse one mapping with different middleware, observer, interceptor, binder, or adapter options without overwriting one another. `describeRoutes()` exposes frozen eligibility snapshots on its cloned descriptors, and dispatcher statistics plus their route entries are frozen observability values.
180
505
 
181
506
  ### Bun decorator bundling compatibility
182
507
 
@@ -186,32 +511,129 @@ This compatibility path is an execution fallback for Bun bundle output; applicat
186
511
 
187
512
  ## Request Cleanup and Portability
188
513
 
189
- The dispatcher binds `RequestContext` with host async-context storage for the active dispatch only. On hosts with `AsyncLocalStorage`, including supported Node 20+ runtimes, the context remains available across awaited work. On non-Node hosts without an async-context primitive, the fallback context is synchronous-only and intentionally unavailable after `await` so overlapping requests cannot observe one another's context. When a request may use request-scoped DI through its controller graph, middleware, guards, interceptors, observers, DTO converters, a custom binder, or manual `getCurrentRequestContext()` / `assertRequestContext()` container access, the dispatcher creates and disposes an isolated request-scoped DI container from its `finally` path after request observers finish. Singleton-only routes skip that container lifecycle until `RequestContext.container` is accessed, so the baseline path avoids unnecessary per-request allocation while preserving request-scoped provider isolation whenever the graph is ambiguous or request-scoped. Public `RequestContext.container` reads are therefore always safe for resolving request-scoped providers; the singleton-only fast path is an internal dispatcher optimization, not a promise that the public context exposes the root container.
514
+ The dispatcher binds `RequestContext` with host async-context storage for the active dispatch only. On hosts with `AsyncLocalStorage`, including supported Node 20+ runtimes, the context remains available across awaited work. On non-Node hosts without an async-context primitive, the fallback context is synchronous-only and intentionally unavailable after `await` so overlapping requests cannot observe one another's context. When a request may use request-scoped DI through its controller graph, middleware, guards, interceptors, observers, DTO converters, a custom binder, or manual `getCurrentRequestContext()` / `assertRequestContext()` container access, the dispatcher creates and disposes an isolated request-scoped DI container from its `finally` path after request observers finish. Routes whose graphs do not require request scope skip that container lifecycle until `RequestContext.container` is accessed, so the baseline path avoids unnecessary per-request allocation while preserving request-scoped provider isolation whenever the graph is ambiguous or request-scoped. The fast path caches handler metadata only and resolves the controller through the active container for every dispatch: singleton providers remain shared by the container, while transient controllers and dependencies retain fresh-per-resolution identity. Public `RequestContext.container` reads are therefore always safe for resolving request-scoped providers; the request-scope-free fast path is an internal dispatcher optimization, not a promise that the public context exposes the root container.
515
+
516
+ Adapters should pass an `AbortSignal` on `FrameworkRequest.signal` when the platform exposes one, or an `isAborted()` probe when allocating a signal is not practical. The dispatcher preserves both abort surfaces on its per-dispatch request clone and treats the request as aborted when either surface reports cancellation, so a `false` probe never masks an aborted signal. It checks both surfaces before and after handler work so adapters without `AbortSignal` can still stop abandoned requests. For SSE, adapters should also expose `FrameworkResponse.stream.onClose(...)` when possible; `SseResponse` listens to both request abort and raw stream close, closes idempotently, and removes registered listeners when either side terminates first.
517
+
518
+ Adapters that parse multipart uploads should attach runtime-neutral `FrameworkRequestFile` values to `FrameworkRequest.files` rather than augmenting the shared HTTP contract with adapter-specific file types. The seam intentionally models the portable fields every HTTP adapter can provide (`fieldname`, `originalname`, `mimetype`, `buffer`, and `size`); platform packages may keep richer native file objects on their raw request surfaces, but guards, binders, middleware, interceptors, and controllers should read files through `RequestContext.request.files` when they need cross-runtime behavior.
519
+
520
+ ### Multipart DTO fields
521
+
522
+ Use `@FromFiles(fieldname?)` with `@RequestDto(...)` when multipart files are part of a handler's input contract:
523
+
524
+ ```ts
525
+ import {
526
+ Controller,
527
+ FromFiles,
528
+ Optional,
529
+ Post,
530
+ RequestDto,
531
+ type FrameworkRequestFile,
532
+ } from '@fluojs/http';
533
+
534
+ class UploadAssetsDto {
535
+ @FromFiles('attachments')
536
+ attachments: readonly FrameworkRequestFile[] = [];
537
+
538
+ @FromFiles('cover')
539
+ @Optional()
540
+ cover?: readonly FrameworkRequestFile[];
541
+ }
542
+
543
+ @Controller('/uploads')
544
+ export class UploadController {
545
+ @Post('/')
546
+ @RequestDto(UploadAssetsDto)
547
+ upload(input: UploadAssetsDto) {
548
+ return input.attachments.map((file) => file.originalname);
549
+ }
550
+ }
551
+ ```
552
+
553
+ `@FromFiles(...)` is array-only: when `FrameworkRequest.files` exists, it returns a readonly array filtered by `fieldname` in adapter arrival order; a present collection without matches becomes `[]`. When the collection is absent, required fields produce the standard missing-field error and `@Optional()` leaves the field `undefined`. Converters and validation receive that same portable array. The DTO binder projects only the five `FrameworkRequestFile` fields, so adapter-native file properties cannot leak through the DTO boundary. Direct `RequestContext.request.files` access remains supported for controllers and pipeline stages that need the entire request collection.
190
554
 
191
- Adapters should pass an `AbortSignal` on `FrameworkRequest.signal` when the platform exposes one, or an `isAborted()` probe when allocating a signal is not practical. The dispatcher preserves both abort surfaces on its per-dispatch request clone and checks them before and after handler work so adapters without `AbortSignal` can still stop abandoned requests. For SSE, adapters should also expose `FrameworkResponse.stream.onClose(...)` when possible; `SseResponse` listens to both request abort and raw stream close, closes idempotently, and removes registered listeners when either side terminates first.
555
+ Response content negotiation formatters must return `string` or `Uint8Array` from `ResponseFormatter.format(...)`. Node.js `Buffer` values remain assignable because `Buffer` implements `Uint8Array`, but formatter contracts should rely only on runtime-neutral byte behavior.
192
556
 
193
557
  ## Public API
194
558
 
195
- - **Routing decorators**: `Controller`, `Get`, `Sse`, `Post`, `Put`, `Patch`, `Delete`, `All`, `Options`, `Head`
196
- - **Binding decorators**: `FromBody`, `FromQuery`, `FromPath`, `FromHeader`, `FromCookie`, `RequestDto`, `Optional`, `Convert`
559
+ - **Routing decorators**: `Controller`, `Get`, `Sse`, `Query`, `Route`, `Post`, `Put`, `Patch`, `Delete`, `All`, `Options`, `Head`
560
+ - **Binding decorators**: `FromBody`, `FromQuery`, `FromPath`, `FromHeader`, `FromCookie`, `FromFiles`, `RequestDto`, `Optional`, `Convert`
197
561
  - **Execution decorators**: `UseGuards`, `UseInterceptors`, `HttpCode`, `Version`, `Header`, `Redirect`, `Produces`
198
- - **Core runtime types**: `RequestContext`, `FrameworkRequest`, `FrameworkResponse`, `SseResponse`, `SseMessage`, `Middleware`, `MiddlewareContext`, `MiddlewareRouteConfig`, `Next`, `Guard`, `GuardContext`, `Interceptor`, `InterceptorContext`, `CallHandler`, `RequestObserver`, `DispatcherLogger`
199
- - **Adapter API**: `HttpApplicationAdapter`, `createNoopHttpApplicationAdapter`, `createServerBackedHttpAdapterRealtimeCapability`, `createUnsupportedHttpAdapterRealtimeCapability`, `createFetchStyleHttpAdapterRealtimeCapability`
200
- - **Exceptions and errors**: `HttpException`, `BadRequestException`, `UnauthorizedException`, `ForbiddenException`, `NotFoundException`, `ConflictException`, `NotAcceptableException`, `TooManyRequestsException`, `InternalServerErrorException`, `PayloadTooLargeException`, `createErrorResponse`, `RouteConflictError`, `InvalidRoutePathError`, `HandlerNotFoundError`, `RequestAbortedError`
201
- - **Helpers**: `createHandlerMapping`, `createDispatcher`, `forRoutes`, `normalizeRoutePattern`, `matchRoutePattern`, `isMiddlewareRouteConfig`, `createCorrelationMiddleware`, `createCorsMiddleware`, `createRateLimitMiddleware`, `createMemoryRateLimitStore`, `createSecurityHeadersMiddleware`, `runWithRequestContext`, `getCurrentRequestContext`, `assertRequestContext`, `createRequestContext`, `createContextKey`, `getContextValue`, `setContextValue`, `encodeSseComment`, `encodeSseMessage`, `isSseMessage`
562
+ - **Header helpers**: `getRequestHeader`, `getResponseHeader`, `hasResponseHeader`, `appendVaryHeader`, `buildContentDisposition`
563
+ - **Response cookie helpers**: `setCookie`, `clearCookie`, `CookieOptions`, `ClearCookieOptions`, `CookieSameSite`
564
+ - **Trusted connection API**: `resolveHttpConnection`, `HttpConnection`, `ResolveHttpConnectionOptions`, `TrustProxyPolicy`, `TrustProxyPredicate`, `FrameworkRequestConnection`
565
+ - **Structured access logging**: `createAccessLogObserver`, `CreateAccessLogObserverOptions`, `AccessLogSink`, `AccessLogEvent`, `AccessLogStartEvent`, `AccessLogErrorEvent`, `AccessLogFinishEvent`, `AccessLogOutcome`, `AccessLogHeaderOptions`, `AccessLogRequestFields`
566
+ - **Conditional request types**: `EntityTagStrength`, `EntityTag`, `ResponseValidators`, `ConditionalRequestContext`, `ConditionalRequestResolution`, `ConditionalRequestResolver`, `ConditionalRequestOptions`
567
+ - **Byte-range responses**: `createByteRangeResponse`, `ByteRangeResponseSource`, `ByteRangeResponseOptions`
568
+ - **Static assets**: `createStaticAssetsMiddleware`, `StaticAssetSource`, `StaticAsset`, `StaticAssetAcceptedEncoding`, `StaticAssetContentEncoding`, `StaticAssetNotAcceptable`, `StaticAssetResolveContext`, `StaticAssetResolution`, `StaticAssetsMiddleware`, `StaticAssetsMiddlewareOptions`. This package owns middleware and source-selection contracts; `@fluojs/platform-nodejs` owns the optional Node filesystem source.
569
+ - **Response transport controls**: `FrameworkResponseSendOptions`, `FrameworkResponseStream` (`onError` reports every transport failure occurrence, including `undefined`, and returns an optional remover that callers invoke after their stream settles)
570
+ - **Request/response and context types**: `RequestContext`, `Principal`, `ContextKey`, `ControllerHandler`, `FrameworkRequest`, `FrameworkRequestFile`, `FrameworkResponse`, `EarlyHintsHeaders`, `FrameworkResponseEarlyHints`, `FrameworkResponseStream`, `FrameworkResponseCompression`, `FrameworkResponseCompressionWriteOptions`, `SseResponse`, `SseMessage`
571
+ - **Dispatcher, routing, and negotiation types**: `Dispatcher`, `CreateDispatcherOptions`, `ErrorHandler`, `DispatcherLogger`, `HandlerMapping`, `HandlerMetadata`, `HandlerDescriptor`, `HandlerMatch`, `HandlerSource`, `RouteDefinition`, `HttpMethod`, `VersioningType`, `VersioningOptions`, `VersioningExtractor`, `VersioningExtractorResult`, `ContentNegotiationOptions`, `ResponseFormatter`, `HttpErrorRepresentationContext`, `HtmlErrorRepresentationProvider`, `HttpErrorRepresentationOptions`, `FastPathEligibility`, `FastPathStats`
572
+ - **Pipeline contract types**: `Middleware`, `MiddlewareLike`, `MiddlewareContext`, `MiddlewareRouteConfig`, `Next`, `Guard`, `GuardLike`, `GuardContext`, `Interceptor`, `InterceptorLike`, `InterceptorContext`, `CallHandler`, `RequestObserver`, `RequestObserverLike`, `RequestObservationContext`, `ArgumentResolverContext`, `Binder`, `Converter`, `ConverterLike`, `ConverterTarget`, `ValidationIssue`, `Validator`
573
+ - **Adapter API**: `HttpApplicationAdapter`, `HttpAdapterRealtimeCapability`, `ServerBackedHttpAdapterRealtimeCapability`, `FetchStyleHttpAdapterRealtimeCapability`, `HttpAdapterRealtimeBindingInstallation`, `UnsupportedHttpAdapterRealtimeCapability`, `createNoopHttpApplicationAdapter`, `createServerBackedHttpAdapterRealtimeCapability`, `createUnsupportedHttpAdapterRealtimeCapability`, `createFetchStyleHttpAdapterRealtimeCapability`
574
+ - **Exceptions and errors**: `HttpExceptionDetail`, `HttpExceptionOptions`, `ErrorResponse`, `HttpException`, `BadRequestException`, `UnauthorizedException`, `ForbiddenException`, `NotFoundException`, `ConflictException`, `NotAcceptableException`, `TooManyRequestsException`, `InternalServerErrorException`, `PayloadTooLargeException`, `createErrorResponse`, `RouteConflictError`, `InvalidRoutePathError`, `InvalidHttpMethodError`, `HandlerNotFoundError`, `RequestAbortedError`, `EarlyHintsWriteError`
575
+ - **Helpers**: `createHandlerMapping`, `createDispatcher`, `forRoutes`, `normalizeRoutePattern`, `matchRoutePattern`, `isMiddlewareRouteConfig`, `createCorrelationMiddleware`, `createCorsMiddleware`, `createRateLimitMiddleware`, `createMemoryRateLimitStore`, `createSecurityHeadersMiddleware`, `getRequestHeader`, `getResponseHeader`, `hasResponseHeader`, `appendVaryHeader`, `buildContentDisposition`, `runWithRequestContext`, `getCurrentRequestContext`, `assertRequestContext`, `createRequestContext`, `createContextKey`, `getContextValue`, `setContextValue`, `encodeSseComment`, `encodeSseMessage`, `isSseMessage`, `formatFastPathStats`, `getDispatcherFastPathStats`, `FAST_PATH_ELIGIBILITY_SYMBOL`, `FAST_PATH_STATS_SYMBOL`
202
576
  - **Option and store types**: `CorsOptions`, `RateLimitOptions`, `RateLimitStore`, `RateLimitStoreEntry`, `SecurityHeadersOptions`, `SseSendOptions`
203
577
 
578
+ ## Portable Subpath (`@fluojs/http/portable`)
579
+
580
+ Use `@fluojs/http/portable` from runtime-neutral integrations that need HTTP authoring contracts without eagerly initializing the Node `AsyncLocalStorage` bootstrap. It exports the supported HTTP decorators, exceptions, request/response contracts, and authoring helpers; Node applications should continue to import the root package when they need its Node request-context behavior.
581
+
204
582
  ## Internal Subpath (`@fluojs/http/internal`)
205
583
 
206
- The `./internal` subpath exports only the low-level utilities used by platform adapters and the core runtime. These are subject to change and should not be used in typical application code.
584
+ The `./internal` subpath exports only the low-level utilities used by platform adapters, the core runtime, and first-party response integrations. These are subject to change and should not be used in typical application code.
207
585
 
208
586
  - `DefaultBinder`: Default DTO/request binder used by the runtime bootstrap path.
209
587
  - `bindRawRequestNativeRouteHandoff(...)` / `attachFrameworkRequestNativeRouteHandoff(...)`: Internal adapter/runtime helpers for reusing semantically safe native route matches without widening the public dispatcher API.
210
588
  - `consumeRawRequestNativeRouteHandoff(...)` / `readFrameworkRequestNativeRouteHandoff(...)`: Internal helpers for reading or consuming native route handoffs.
211
589
  - Native route handoffs snapshot the framework request method and path when attached; if app middleware rewrites either value before handler matching, the dispatcher ignores the stale handoff and falls back to normal route matching.
212
590
  - `isRoutePathNormalizationSensitive(path)`: Internal guard for keeping duplicate-slash and trailing-slash requests on the generic dispatcher path.
591
+ - `getCompiledRouteIdentity(descriptor)`: Reads the deterministic source/method position assigned by `createHandlerMapping(...)` for first-party package integrations. Manually authored descriptors return `undefined`.
213
592
  - `resolveClientIdentity(request)`: Conservative client identity resolver used by rate limiting and other runtime integrations.
214
593
  - `createFetchStyleHttpAdapterRealtimeCapability(...)`, `Dispatcher`, and `HttpApplicationAdapter`: internal adapter seams for edge/fetch-style platform packages that must avoid instantiating the full HTTP root barrel.
594
+ - `FRAMEWORK_RESPONSE_WRITER` / `registerFrameworkResponseWriter(...)`: Typed response-entry branding seam for first-party response integrations.
595
+ - `FRAMEWORK_RESPONSE_VALUE_FINALIZER` / `registerFrameworkResponseValueFinalizer(...)`: Typed request-local response finalization seam. Finalizers compose in registration order, each receives the prior resolved value, and the dispatcher awaits them so throws and rejections follow its normal error policy.
596
+
597
+ ## Conditional Requests
598
+
599
+ Configure `conditionalRequest` during runtime bootstrap to resolve representation existence separately from optional validators:
600
+
601
+ ```ts
602
+ const app = await bootstrapNodeApplication(AppModule, {
603
+ conditionalRequest: {
604
+ resolve({ handler, request }) {
605
+ return {
606
+ exists: true,
607
+ validators: {
608
+ etag: { opaqueValue: `${handler.route.method}:${request.path}:v1`, strength: 'strong' },
609
+ lastModified: new Date('2026-01-01T00:00:00Z'),
610
+ },
611
+ };
612
+ },
613
+ },
614
+ });
615
+ ```
616
+
617
+ Return `{ exists: false }` when no representation exists. Return `{ exists: true }` when it exists but intentionally has no validators. The dispatcher evaluates this resolver after application/module middleware and guards, so conditional `304` and `412` responses never bypass authorization or audit logic. It accepts only valid entity-tag lists and HTTP-date forms; malformed conditional fields are ignored.
618
+
619
+ The dispatcher owns RFC 9110 precedence and comparison: a successful `If-Match` skips only `If-Unmodified-Since`, then `If-None-Match` still takes precedence over `If-Modified-Since`; `If-Match` uses strong comparison and `If-None-Match` weak comparison. `304` and `412` are bodyless and retain `ETag`/`Last-Modified`, including redirect and supported custom response-writer paths. For the same selected representation, `HEAD` and `GET` use the same conditional result and framework-generated `HEAD` bodies are suppressed. An explicit `@Head` route remains an independent route; custom response writers own their body emission and must preserve the `HEAD` bodyless contract themselves. See the [HTTP Runtime Contract](../../docs/architecture/http-runtime.md).
620
+
621
+ ## Byte Range Responses
622
+
623
+ Returning a `Uint8Array` or `ArrayBuffer` enables RFC single-range `bytes` responses automatically for `GET` and the `HEAD` metadata mirror. A valid range produces `206`, `Accept-Ranges: bytes`, `Content-Range`, and the exact identity-byte `Content-Length`; malformed or multi-range fields fall back to the complete representation, while unsatisfiable ranges return bodyless `416` with `Accept-Ranges: bytes`, `Content-Range: bytes */<size>`, and `Content-Length: 0`. `POST`, unsafe, and custom methods ignore `Range` and retain their ordinary full status, body, and metadata.
624
+
625
+ Use `createByteRangeResponse(...)` for a portable `ReadableStream` and provide its exact full size. Pass a factory when `HEAD` must not construct the stream:
626
+
627
+ ```ts
628
+ import { createByteRangeResponse } from '@fluojs/http';
629
+
630
+ return createByteRangeResponse(
631
+ () => file.stream(),
632
+ { contentType: 'image/png', size: file.size },
633
+ );
634
+ ```
635
+
636
+ The dispatcher evaluates normal conditional requests first. `If-Range` then permits a partial response only for an exact strong `ETag` or a current `Last-Modified` date; otherwise it sends the complete representation. Partial responses preserve identity bytes and bypass Node compression so range offsets and lengths remain meaningful. `HEAD` preserves GET status and metadata without opening the stream. The application owns its bytes, exact size, and any filesystem resource: `createByteRangeResponse(...)` never opens, stats, seeks, sizes, or closes files, and it intentionally does not construct multi-range responses.
215
637
 
216
638
  ## Related Packages
217
639