@fluojs/platform-fastify 1.0.0-beta.7 → 1.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.
package/README.ko.md CHANGED
@@ -40,11 +40,15 @@ const app = await fluoFactory.create(AppModule, {
40
40
  await app.listen();
41
41
  ```
42
42
 
43
+ `createFastifyAdapter()`는 기본 port로 `3000`을 사용하며 `process.env.PORT`를 읽지 않습니다. 잘못된 explicit port 값은 adapter setup 중 throw됩니다.
44
+
43
45
  ## 주요 패턴
44
46
 
45
47
  ### 멀티파트 및 Raw Body
46
48
  Fastify 어댑터는 내부 Fastify 플러그인을 통해 멀티파트 form-data 및 raw body 파싱을 기본적으로 지원하며, 이는 표준 fluo 요청 인터페이스를 통해 노출됩니다. `rawBody: true`를 활성화하면 멀티파트가 아닌 요청에서 `FrameworkRequest.rawBody`가 원본 요청 바이트를 그대로 보존하므로 webhook 서명 검증이나 기타 바이트 민감한 흐름에서 정확한 payload를 다시 사용할 수 있습니다. 어댑터를 직접 생성할 때는 멀티파트 제한을 두 번째 인자로 전달하고, `bootstrapFastifyApplication(...)` 및 `runFastifyApplication(...)`에서는 같은 설정을 `options.multipart` 아래에 전달하면 됩니다.
47
49
 
50
+ Multipart request에서는 raw-body capture를 건너뜁니다. `multipart.maxTotalSize`를 생략하면 `maxBodySize`가 기본값이 되어 HTTP adapter 간 size limit이 portable하게 유지됩니다.
51
+
48
52
  ```typescript
49
53
  const adapter = createFastifyAdapter(
50
54
  {
@@ -126,7 +130,7 @@ await bootstrapFastifyApplication(AppModule, {
126
130
  ```
127
131
 
128
132
  ### 네이티브 라우트 등록과 안전한 폴백
129
- fluo 라우트 메타데이터를 Fastify 경로로 그대로 옮길 수 있는 경우, 어댑터는 모든 요청을 단일 와일드카드 라우트로 보내는 대신 Fastify 네이티브 per-route 핸들러를 등록합니다. 의미 보존이 가능한 unversioned route에서는 Fastify가 미리 고른 descriptor와 params를 공유 fluo dispatcher에 전달하므로 duplicate route matching을 건너뛰면서도 middleware, guards, interceptors, observers, SSE, multipart, raw body, streaming, error handling 의미론은 그대로 유지됩니다.
133
+ fluo 라우트 메타데이터를 Fastify 경로로 그대로 옮길 수 있는 경우, 어댑터는 모든 요청을 단일 와일드카드 라우트로 보내는 대신 명시적 `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD` route에 Fastify 네이티브 per-route 핸들러를 등록합니다. 의미 보존이 가능한 unversioned route에서는 Fastify가 미리 고른 descriptor와 params를 공유 fluo dispatcher에 전달하므로 duplicate route matching을 건너뛰면서도 middleware, guards, interceptors, observers, SSE, multipart, raw body, streaming, error handling 의미론은 그대로 유지됩니다.
130
134
 
131
135
  여러 라우트가 같은 method와 정규화된 param shape를 공유하는 경우(예: `/:id` 와 `/:slug`), `@All(...)`을 사용하는 경우, non-URI versioning에 의존하는 경우, 또는 duplicate slash / trailing slash 변형으로 들어온 경우에는 어댑터가 해당 요청을 의도적으로 와일드카드 fallback 경로에 남겨 둡니다. 이렇게 해서 Fastify 등록 단계에서 부팅 실패가 나거나 fluo의 등록 순서 기반 매칭 의미론이 좁아지지 않도록 보장합니다. app middleware가 native handoff 이후 framework request의 method 또는 path를 rewrite하면 dispatcher는 stale handoff를 무시하고 rewrite된 요청을 다시 매칭합니다.
132
136
 
@@ -148,7 +152,9 @@ fluo의 Fastify 어댑터는 높은 동시성 시나리오에서 raw Node.js 어
148
152
  - `createFastifyAdapter(options)`: Fastify 어댑터를 위한 권장 팩토리입니다.
149
153
  - `bootstrapFastifyApplication(module, options)`: 암시적 리스닝 없이 수행하는 고급 부트스트랩입니다.
150
154
  - `runFastifyApplication(module, options)`: 생명주기 관리를 포함한 빠른 시작 헬퍼입니다. timeout/실패 시에는 해당 상태를 로그와 `process.exitCode`로 보고하고, 최종 프로세스 종료는 주변 호스트에 맡깁니다.
155
+ - `isFastifyMultipartTooLargeError(error)`: Fastify error shape 전반에서 multipart limit error를 감지합니다.
151
156
  - `FastifyHttpApplicationAdapter`: 핵심 어댑터 구현 클래스입니다.
157
+ - Option type: `FastifyAdapterOptions`, `BootstrapFastifyApplicationOptions`, `RunFastifyApplicationOptions`, `CorsInput`, `FastifyApplicationSignal`.
152
158
 
153
159
  ## 트러블슈팅
154
160
 
@@ -156,6 +162,7 @@ fluo의 Fastify 어댑터는 높은 동시성 시나리오에서 raw Node.js 어
156
162
  - **미들웨어 문제**: `middleware` 옵션은 런타임 레벨의 `MiddlewareLike[]` 함수 배열을 받습니다. 이는 Fastify 플러그인이 아니며 다른 fluo 어댑터들과 공통으로 사용되는 표준 인터페이스를 따릅니다.
157
163
  - **로깅 (Logging)**: 로그 스트림 중복을 방지하기 위해 Fastify의 네이티브 로거가 비활성화됩니다. 모든 로깅 설정은 `runFastifyApplication` 또는 `bootstrapFastifyApplication`의 `logger` 옵션을 통해 이루어져야 합니다.
158
164
  - **글로벌 접두사 (Global Prefix)**: 내부 경로 또는 헬스 체크 엔드포인트에 접두사가 붙지 않도록 `globalPrefixExclude`를 적절히 설정하세요.
165
+ - **Malformed Cookie**: 잘못된 cookie header는 request 실패로 이어지지 않고 보존됩니다.
159
166
 
160
167
  ## 관련 패키지
161
168
 
package/README.md CHANGED
@@ -40,11 +40,15 @@ const app = await fluoFactory.create(AppModule, {
40
40
  await app.listen();
41
41
  ```
42
42
 
43
+ `createFastifyAdapter()` defaults to port `3000` and does not read `process.env.PORT`; invalid explicit port values throw during adapter setup.
44
+
43
45
  ## Common Patterns
44
46
 
45
47
  ### Multipart and Raw Body
46
48
  The Fastify adapter includes built-in support for multipart form-data and raw body parsing via internal Fastify plugins, exposed through the standard fluo request interface. When `rawBody: true` is enabled, `FrameworkRequest.rawBody` preserves the original request bytes for non-multipart requests so webhook signature verification and other byte-sensitive flows can replay the exact payload. When you construct the adapter directly, pass multipart limits as the second argument. `bootstrapFastifyApplication(...)` and `runFastifyApplication(...)` accept the same multipart settings under `options.multipart`.
47
49
 
50
+ Raw-body capture is skipped for multipart requests. When `multipart.maxTotalSize` is omitted, it defaults to `maxBodySize` so size limits stay portable across HTTP adapters.
51
+
48
52
  ```typescript
49
53
  const adapter = createFastifyAdapter(
50
54
  {
@@ -126,7 +130,7 @@ await bootstrapFastifyApplication(AppModule, {
126
130
  ```
127
131
 
128
132
  ### Native Route Registration with Safe Fallback
129
- When fluo route metadata can be translated directly, the adapter registers Fastify-native per-route handlers instead of sending every request through a single wildcard route. For semantically safe unversioned routes, those native handlers hand a pre-matched descriptor and params to the shared fluo dispatcher so duplicate route matching is skipped without changing framework-owned guards, interceptors, observers, SSE, multipart, raw body, streaming, or error handling.
133
+ When fluo route metadata can be translated directly, the adapter registers Fastify-native per-route handlers for explicit `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, and `HEAD` routes instead of sending every request through a single wildcard route. For semantically safe unversioned routes, those native handlers hand a pre-matched descriptor and params to the shared fluo dispatcher so duplicate route matching is skipped without changing framework-owned guards, interceptors, observers, SSE, multipart, raw body, streaming, or error handling.
130
134
 
131
135
  When multiple routes share the same method and normalized param shape (for example `/:id` and `/:slug`), use `@All(...)`, depend on non-URI versioning, or arrive through duplicate-slash / trailing-slash variants, the adapter intentionally leaves those requests on the wildcard fallback path so Fastify registration cannot boot-fail or narrow fluo's matching semantics. If app middleware rewrites the framework request method or path after a native handoff was attached, the dispatcher ignores that stale handoff and rematches the rewritten request.
132
136
 
@@ -148,7 +152,9 @@ fluo's Fastify adapter significantly outperforms the raw Node.js adapter in high
148
152
  - `createFastifyAdapter(options)`: Recommended factory for the Fastify adapter.
149
153
  - `bootstrapFastifyApplication(module, options)`: advanced bootstrap without implicit listening.
150
154
  - `runFastifyApplication(module, options)`: Quick-start helper with lifecycle management. On timeout/failure it reports the condition through logging and `process.exitCode`, while leaving final process termination to the surrounding host.
155
+ - `isFastifyMultipartTooLargeError(error)`: Detects multipart limit errors across Fastify error shapes.
151
156
  - `FastifyHttpApplicationAdapter`: The core adapter implementation.
157
+ - Option types: `FastifyAdapterOptions`, `BootstrapFastifyApplicationOptions`, `RunFastifyApplicationOptions`, `CorsInput`, `FastifyApplicationSignal`.
152
158
 
153
159
  ## Troubleshooting
154
160
 
@@ -156,6 +162,7 @@ fluo's Fastify adapter significantly outperforms the raw Node.js adapter in high
156
162
  - **Middleware Issues**: The `middleware` option accepts runtime-level `MiddlewareLike[]` functions. These are not Fastify plugins and follow the standard middleware interface used across fluo adapters.
157
163
  - **Logging**: The native Fastify logger is disabled to prevent duplicate log streams. All logging should be configured via the fluo `logger` option in `runFastifyApplication` or `bootstrapFastifyApplication`.
158
164
  - **Global Prefix**: Use `globalPrefixExclude` to prevent the prefix from being applied to internal routes or health check endpoints.
165
+ - **Malformed Cookies**: Malformed cookie headers are preserved rather than failing the request.
159
166
 
160
167
  ## Related Packages
161
168
 
package/dist/adapter.d.ts CHANGED
@@ -88,6 +88,7 @@ export declare class FastifyHttpApplicationAdapter implements HttpApplicationAda
88
88
  private registerWildcardFallbackRoute;
89
89
  private listenWithRetry;
90
90
  private handleRequest;
91
+ private handleNativeRouteRequest;
91
92
  }
92
93
  /**
93
94
  * Create the recommended Fastify adapter for `FluoFactory.create(...)`.
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
package/dist/adapter.js CHANGED
@@ -19,6 +19,7 @@ import { dispatchWithRequestResponseFactory } from '@fluojs/runtime/internal/req
19
19
  const DEFAULT_MAX_BODY_SIZE = 1 * 1024 * 1024;
20
20
  const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;
21
21
  const FASTIFY_NATIVE_ROUTE_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'];
22
+ const EMPTY_NATIVE_ROUTE_PARAMS = Object.freeze({});
22
23
 
23
24
  /**
24
25
  * Bootstrap options for creating a Fastify-backed application without
@@ -104,13 +105,11 @@ export class FastifyHttpApplicationAdapter {
104
105
  for (const route of createFastifyNativeRoutes(descriptors)) {
105
106
  this.app.route({
106
107
  handler: async (request, reply) => {
107
- const requestPath = readRequestPathFromRawUrl(request.raw.url);
108
+ const urlParts = splitRawRequestUrl(request.raw.url ?? '/');
108
109
  const params = normalizeNativeRouteParams(request.params);
109
- if (!isRoutePathNormalizationSensitive(requestPath) && !hasNativeRouteParamSeparators(params)) {
110
- bindRawRequestNativeRouteHandoff(request.raw, {
111
- descriptor: route.descriptor,
112
- params
113
- });
110
+ if (!isRoutePathNormalizationSensitive(urlParts.path) && !hasNativeRouteParamSeparators(params)) {
111
+ await this.handleNativeRouteRequest(route.descriptor, params, urlParts, request, reply);
112
+ return;
114
113
  }
115
114
  await this.handleRequest(request, reply);
116
115
  },
@@ -149,6 +148,128 @@ export class FastifyHttpApplicationAdapter {
149
148
  rawResponse: reply
150
149
  });
151
150
  }
151
+ async handleNativeRouteRequest(descriptor, params, urlParts, request, reply) {
152
+ if (isMultipartRequestContentType(request.raw.headers['content-type'])) {
153
+ bindRawRequestNativeRouteHandoff(request.raw, {
154
+ descriptor,
155
+ params
156
+ });
157
+ await this.handleRequest(request, reply);
158
+ return;
159
+ }
160
+ const dispatcher = this.dispatcher;
161
+ if (!dispatcher?.dispatchNativeRoute) {
162
+ bindRawRequestNativeRouteHandoff(request.raw, {
163
+ descriptor,
164
+ params
165
+ });
166
+ await this.handleRequest(request, reply);
167
+ return;
168
+ }
169
+ const factory = this.requestResponseFactory;
170
+ const frameworkResponse = factory.createResponse(reply, request);
171
+ const lazySignal = createLazyFastifyRequestSignal(reply, factory.createRequestSignal);
172
+ try {
173
+ const frameworkRequest = createNativeFastFrameworkRequest(request, lazySignal, urlParts, this.maxBodySize, this.preserveRawBody) ?? (await factory.createRequest(request, lazySignal.signal()));
174
+ if (!isNativeFastFrameworkRequest(frameworkRequest)) {
175
+ await factory.materializeRequest?.(frameworkRequest);
176
+ }
177
+ const handled = await dispatcher.dispatchNativeRoute({
178
+ descriptor,
179
+ params
180
+ }, frameworkRequest, frameworkResponse);
181
+ if (!handled) {
182
+ bindRawRequestNativeRouteHandoff(request.raw, {
183
+ descriptor,
184
+ params
185
+ });
186
+ await this.handleRequest(request, reply);
187
+ return;
188
+ }
189
+ if (!frameworkResponse.committed) {
190
+ await frameworkResponse.send(undefined);
191
+ }
192
+ } catch (error) {
193
+ if (lazySignal.isAborted() || frameworkResponse.committed) {
194
+ return;
195
+ }
196
+ await factory.writeErrorResponse(error, frameworkResponse, factory.resolveRequestId(request));
197
+ }
198
+ }
199
+ }
200
+ function createNativeFastFrameworkRequest(request, lazySignal, urlParts, maxBodySize, preserveRawBody) {
201
+ const contentType = request.headers['content-type'];
202
+ if (preserveRawBody || isFastifyMultipartRequest(request) || isMultipartRequestContentType(contentType)) {
203
+ return undefined;
204
+ }
205
+ const contentLength = Number(request.headers['content-length']);
206
+ if (Number.isFinite(contentLength) && contentLength > maxBodySize) {
207
+ throw new PayloadTooLargeException('Request body exceeds the size limit.');
208
+ }
209
+ const frameworkRequest = createDeferredFrameworkRequestShell({
210
+ cookieHeader: cloneHeaderValue(request.headers.cookie),
211
+ headersFactory: () => normalizeHeaders(cloneRequestHeaders(request.headers)),
212
+ method: request.method,
213
+ path: urlParts.path,
214
+ query: readSimpleQueryRecord(request.query),
215
+ queryFactory: () => parseQueryParamsFromSearch(urlParts.search),
216
+ raw: request.raw,
217
+ requestId: resolvePrimaryRequestIdFromHeaders(request.raw.headers),
218
+ signal: lazySignal.signal,
219
+ url: urlParts.path + urlParts.search
220
+ });
221
+ frameworkRequest.body = request.body;
222
+ frameworkRequest.isAborted = lazySignal.isAborted;
223
+ markNativeFastFrameworkRequest(frameworkRequest);
224
+ return frameworkRequest;
225
+ }
226
+ function createLazyFastifyRequestSignal(reply, signalFactory) {
227
+ let signal;
228
+ return {
229
+ isAborted() {
230
+ return signal?.aborted ?? isFastifyReplyAborted(reply);
231
+ },
232
+ signal() {
233
+ if (!signal) {
234
+ signal = isFastifyReplyAborted(reply) ? AbortSignal.abort(new Error('Response closed before response commit.')) : signalFactory(reply);
235
+ }
236
+ return signal;
237
+ }
238
+ };
239
+ }
240
+ function isFastifyReplyAborted(reply) {
241
+ return reply.raw.destroyed && !reply.raw.writableEnded;
242
+ }
243
+ const NATIVE_FAST_FRAMEWORK_REQUEST = Symbol('fluo.fastify.nativeFastFrameworkRequest');
244
+ function markNativeFastFrameworkRequest(request) {
245
+ request[NATIVE_FAST_FRAMEWORK_REQUEST] = true;
246
+ }
247
+ function isNativeFastFrameworkRequest(request) {
248
+ return request[NATIVE_FAST_FRAMEWORK_REQUEST] === true;
249
+ }
250
+ function isFastifyMultipartRequest(request) {
251
+ const probe = request.isMultipart;
252
+ return typeof probe === 'function' && probe.call(request) === true;
253
+ }
254
+ function readSimpleQueryRecord(query) {
255
+ if (typeof query !== 'object' || query === null) {
256
+ return undefined;
257
+ }
258
+ const record = query;
259
+ for (const key in record) {
260
+ if (!Object.prototype.hasOwnProperty.call(record, key)) {
261
+ continue;
262
+ }
263
+ const value = record[key];
264
+ if (typeof value === 'string') {
265
+ continue;
266
+ }
267
+ if (Array.isArray(value) && value.every(item => typeof item === 'string')) {
268
+ continue;
269
+ }
270
+ return undefined;
271
+ }
272
+ return record;
152
273
  }
153
274
  function createFastifyRequestResponseFactory(multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false) {
154
275
  return {
@@ -271,65 +392,71 @@ export async function runFastifyApplication(rootModule, options) {
271
392
  shutdownRegistration: createNodeShutdownSignalRegistration(options.shutdownSignals ?? defaultNodeShutdownSignals())
272
393
  }, adapter);
273
394
  }
274
- function createFrameworkResponse(reply) {
275
- let activeStream;
276
- return {
277
- committed: reply.sent,
278
- headers: {},
279
- raw: reply,
280
- get stream() {
281
- activeStream ??= createFrameworkResponseStream(reply);
282
- return activeStream;
283
- },
284
- redirect(status, location) {
285
- this.setStatus(status);
286
- this.setHeader('Location', location);
395
+ class MutableFastifyFrameworkResponse {
396
+ committed;
397
+ headers = {};
398
+ raw;
399
+ statusCode;
400
+ statusSet = false;
401
+ activeStream;
402
+ constructor(reply) {
403
+ this.reply = reply;
404
+ this.committed = reply.sent;
405
+ this.raw = reply;
406
+ }
407
+ get stream() {
408
+ this.activeStream ??= createFrameworkResponseStream(this.reply);
409
+ return this.activeStream;
410
+ }
411
+ redirect(status, location) {
412
+ this.setStatus(status);
413
+ this.setHeader('Location', location);
414
+ this.committed = true;
415
+ this.reply.redirect(location, status);
416
+ }
417
+ send(body) {
418
+ if (this.reply.sent) {
287
419
  this.committed = true;
288
- reply.redirect(location, status);
289
- },
290
- async send(body) {
291
- if (reply.sent) {
292
- this.committed = true;
293
- return;
294
- }
295
- const existingContentType = reply.getHeader('content-type');
296
- const serialized = serializeResponseBody(body, typeof existingContentType === 'string' ? existingContentType : undefined);
297
- if (!reply.hasHeader('content-type') && serialized.defaultContentType) {
298
- reply.header('content-type', serialized.defaultContentType);
299
- }
300
- this.committed = true;
301
- await reply.send(serialized.payload);
302
- },
303
- async sendSimpleJson(body) {
304
- if (reply.sent) {
305
- this.committed = true;
306
- return;
307
- }
308
- if (!reply.hasHeader('content-type')) {
309
- reply.header('content-type', 'application/json; charset=utf-8');
310
- }
420
+ return;
421
+ }
422
+ const existingContentType = this.reply.getHeader('content-type');
423
+ const serialized = serializeResponseBody(body, typeof existingContentType === 'string' ? existingContentType : undefined);
424
+ if (!this.reply.hasHeader('content-type') && serialized.defaultContentType) {
425
+ this.reply.header('content-type', serialized.defaultContentType);
426
+ }
427
+ this.committed = true;
428
+ void this.reply.send(serialized.payload);
429
+ }
430
+ sendSimpleJson(body) {
431
+ if (this.reply.sent) {
311
432
  this.committed = true;
312
- await reply.send(JSON.stringify(body));
313
- },
314
- setHeader(name, value) {
315
- const lowerName = name.toLowerCase();
316
- if (lowerName === 'set-cookie') {
317
- const merged = mergeSetCookieHeader(reply.getHeader(name), value);
318
- reply.header(name, merged);
319
- this.headers[name] = merged;
320
- return;
321
- }
322
- reply.header(name, value);
323
- this.headers[name] = value;
324
- },
325
- setStatus(code) {
326
- reply.status(code);
327
- this.statusCode = code;
328
- this.statusSet = true;
329
- },
330
- statusCode: undefined,
331
- statusSet: false
332
- };
433
+ return;
434
+ }
435
+ if (!this.reply.hasHeader('content-type')) {
436
+ this.reply.header('content-type', 'application/json; charset=utf-8');
437
+ }
438
+ this.committed = true;
439
+ void this.reply.send(JSON.stringify(body));
440
+ }
441
+ setHeader(name, value) {
442
+ const lowerName = name.toLowerCase();
443
+ if (lowerName === 'set-cookie') {
444
+ const merged = mergeSetCookieHeader(this.reply.getHeader(name), value);
445
+ this.reply.header(name, merged);
446
+ this.headers[name] = merged;
447
+ return;
448
+ }
449
+ this.reply.header(name, value);
450
+ this.headers[name] = value;
451
+ }
452
+ setStatus(code) {
453
+ this.reply.status(code);
454
+ this.statusCode = code;
455
+ this.statusSet = true;
456
+ }
457
+ }
458
+ function createFrameworkResponse(reply) {
459
+ return new MutableFastifyFrameworkResponse(reply);
333
460
  }
334
461
  function createFrameworkResponseStream(reply) {
335
462
  let hijacked = false;
@@ -399,7 +526,8 @@ function createDeferredFrameworkRequest(request, signal, multipartOptions, maxBo
399
526
  const querySnapshot = snapshotSimpleQueryRecord(request.query);
400
527
  const isMultipart = isMultipartRequestContentType(headerSnapshot['content-type']);
401
528
  let frameworkRequest;
402
- const materializeBody = createMemoizedAsyncValue(async () => {
529
+ const needsDeferredBodyMaterialization = isMultipart || preserveRawBody;
530
+ const materializeBody = needsDeferredBodyMaterialization ? createMemoizedAsyncValue(async () => {
403
531
  let body = request.body;
404
532
  let files;
405
533
  if (isMultipart) {
@@ -420,7 +548,7 @@ function createDeferredFrameworkRequest(request, signal, multipartOptions, maxBo
420
548
  frameworkRequest.rawBody = rawBodyValue;
421
549
  }
422
550
  }
423
- });
551
+ }) : undefined;
424
552
  frameworkRequest = createDeferredFrameworkRequestShell({
425
553
  cookieHeader: cloneHeaderValue(headerSnapshot.cookie),
426
554
  headersFactory: headers,
@@ -430,9 +558,13 @@ function createDeferredFrameworkRequest(request, signal, multipartOptions, maxBo
430
558
  query: querySnapshot,
431
559
  queryFactory: () => parseQueryParamsFromSearch(urlParts.search),
432
560
  raw: request.raw,
561
+ requestId: resolvePrimaryRequestIdFromHeaders(headerSnapshot),
433
562
  signal,
434
563
  url: urlParts.path + urlParts.search
435
564
  });
565
+ if (!needsDeferredBodyMaterialization) {
566
+ frameworkRequest.body = request.body;
567
+ }
436
568
  const nativeRouteHandoff = consumeRawRequestNativeRouteHandoff(request.raw);
437
569
  return nativeRouteHandoff ? attachFrameworkRequestNativeRouteHandoff(frameworkRequest, nativeRouteHandoff) : frameworkRequest;
438
570
  }
@@ -442,12 +574,29 @@ async function materializeFrameworkRequestBody(request) {
442
574
  }
443
575
  function normalizeNativeRouteParams(params) {
444
576
  if (typeof params !== 'object' || params === null) {
445
- return {};
577
+ return EMPTY_NATIVE_ROUTE_PARAMS;
578
+ }
579
+ let normalized;
580
+ for (const key in params) {
581
+ if (!Object.prototype.hasOwnProperty.call(params, key)) {
582
+ continue;
583
+ }
584
+ const value = params[key];
585
+ if (value === undefined) {
586
+ continue;
587
+ }
588
+ normalized ??= {};
589
+ normalized[key] = typeof value === 'string' ? value : String(value);
446
590
  }
447
- return Object.fromEntries(Object.entries(params).flatMap(([key, value]) => typeof value === 'string' ? [[key, value]] : value === undefined ? [] : [[key, String(value)]]));
591
+ return normalized ?? EMPTY_NATIVE_ROUTE_PARAMS;
448
592
  }
449
593
  function hasNativeRouteParamSeparators(params) {
450
- return Object.values(params).some(value => value.includes('/'));
594
+ for (const key in params) {
595
+ if (Object.prototype.hasOwnProperty.call(params, key) && params[key]?.includes('/')) {
596
+ return true;
597
+ }
598
+ }
599
+ return false;
451
600
  }
452
601
  function collectVersionSensitiveRouteKeys(descriptors) {
453
602
  const grouped = new Map();
@@ -466,9 +615,6 @@ function collectVersionSensitiveRouteKeys(descriptors) {
466
615
  }
467
616
  return new Set([...grouped.entries()].filter(([, current]) => current.count > 1 || current.hasVersioned).map(([routeKey]) => routeKey));
468
617
  }
469
- function readRequestPathFromRawUrl(rawUrl) {
470
- return splitRawRequestUrl(rawUrl ?? '/').path;
471
- }
472
618
  async function parseMultipartRequest(request, options = {}) {
473
619
  const fields = {};
474
620
  const files = [];
@@ -595,6 +741,10 @@ function resolveRequestIdFromHeaders(headers) {
595
741
  const requestId = headers['x-request-id'] ?? headers['x-correlation-id'];
596
742
  return Array.isArray(requestId) ? requestId[0] : requestId;
597
743
  }
744
+ function resolvePrimaryRequestIdFromHeaders(headers) {
745
+ const requestId = headers['x-request-id'];
746
+ return Array.isArray(requestId) ? requestId[0] : requestId;
747
+ }
598
748
  function createFastifyApp(httpsOptions, maxBodySize) {
599
749
  if (httpsOptions) {
600
750
  return fastify({
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "platform",
9
9
  "server"
10
10
  ],
11
- "version": "1.0.0-beta.7",
11
+ "version": "1.0.0",
12
12
  "private": false,
13
13
  "license": "MIT",
14
14
  "repository": {
@@ -38,12 +38,12 @@
38
38
  "@fastify/multipart": "^9.2.1",
39
39
  "fastify": "^5.8.5",
40
40
  "fastify-raw-body": "^5.0.0",
41
- "@fluojs/http": "^1.0.0-beta.5",
42
- "@fluojs/runtime": "^1.0.0-beta.7"
41
+ "@fluojs/http": "^1.0.0",
42
+ "@fluojs/runtime": "^1.0.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "vitest": "^3.2.4",
46
- "@fluojs/di": "^1.0.0-beta.5"
46
+ "@fluojs/di": "^1.0.0"
47
47
  },
48
48
  "scripts": {
49
49
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",