@fluojs/runtime 1.0.0-beta.6 → 1.0.0-beta.8

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.
@@ -1,6 +1,25 @@
1
1
  import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { PayloadTooLargeException, type FrameworkRequest } from '@fluojs/http';
3
3
  import { type MultipartOptions } from '../multipart.js';
4
+ type MemoizedValue<T> = () => T;
5
+ type QueryRecord = Record<string, string | string[] | undefined>;
6
+ /**
7
+ * Options for creating a deferred framework request shell from a Node-backed adapter.
8
+ */
9
+ export interface DeferredFrameworkRequestShellOptions<RawRequest> {
10
+ cookieHeader?: string | string[] | undefined;
11
+ headers?: FrameworkRequest['headers'];
12
+ headersFactory?: () => FrameworkRequest['headers'];
13
+ materializeBody?: () => Promise<void>;
14
+ method?: string;
15
+ path: string;
16
+ query?: QueryRecord;
17
+ queryFactory?: () => QueryRecord;
18
+ raw: RawRequest;
19
+ requestId?: string;
20
+ signal: AbortSignal | (() => AbortSignal);
21
+ url: string;
22
+ }
4
23
  /**
5
24
  * HTTP payload-size error that closes the underlying Node request stream after the response commits.
6
25
  */
@@ -31,6 +50,13 @@ export declare function createFrameworkRequest(request: IncomingMessage, signal:
31
50
  * @returns The framework request shell with metadata snapshotted and body materialization deferred.
32
51
  */
33
52
  export declare function createDeferredFrameworkRequest(request: IncomingMessage, signal: AbortSignal, multipartOptions?: MultipartOptions, maxBodySize?: number, preserveRawBody?: boolean): FrameworkRequest;
53
+ /**
54
+ * Creates a framework request shell from already-snapshotted Node adapter metadata.
55
+ *
56
+ * @param options - Raw request, metadata factories, and deferred body materialization hooks.
57
+ * @returns A framework request with lazy headers, cookies, query values, and optional body materialization.
58
+ */
59
+ export declare function createDeferredFrameworkRequestShell<RawRequest>({ cookieHeader, headers, headersFactory, materializeBody, method, path, query, queryFactory, raw, requestId, signal, url, }: DeferredFrameworkRequestShellOptions<RawRequest>): FrameworkRequest;
34
60
  /**
35
61
  * Materializes a deferred Node framework request body exactly once.
36
62
  *
@@ -38,6 +64,20 @@ export declare function createDeferredFrameworkRequest(request: IncomingMessage,
38
64
  * @returns A promise that settles after body, rawBody, and files fields are populated when applicable.
39
65
  */
40
66
  export declare function materializeFrameworkRequestBody(request: FrameworkRequest): Promise<void>;
67
+ /**
68
+ * Creates a synchronous memoized value resolver.
69
+ *
70
+ * @param factory - Function that computes the value on first access.
71
+ * @returns A stable resolver that returns the cached value after the first call.
72
+ */
73
+ export declare function createMemoizedValue<T>(factory: () => T): MemoizedValue<T>;
74
+ /**
75
+ * Creates an async memoized side-effect resolver.
76
+ *
77
+ * @param factory - Async function to run at most once.
78
+ * @returns A resolver that returns the same in-flight or completed promise for every call.
79
+ */
80
+ export declare function createMemoizedAsyncValue(factory: () => Promise<void>): () => Promise<void>;
41
81
  /**
42
82
  * Creates an abort signal that fires when the Node response closes unexpectedly.
43
83
  *
@@ -52,4 +92,71 @@ export declare function createRequestSignal(response: ServerResponse): AbortSign
52
92
  * @returns The request identifier when present.
53
93
  */
54
94
  export declare function resolveRequestIdFromHeaders(headers: IncomingHttpHeaders): string | undefined;
95
+ /**
96
+ * Parses a raw URL search string into the framework query shape.
97
+ *
98
+ * @param search - Raw search string, with or without a leading question mark.
99
+ * @returns Query values where repeated keys become string arrays.
100
+ */
101
+ export declare function parseQueryParamsFromSearch(search: string): Record<string, string | string[]>;
102
+ /**
103
+ * Snapshots host-parsed query values when they already match framework semantics.
104
+ *
105
+ * @param query - Host query object exposed by a Node-backed adapter.
106
+ * @returns A cloned query record when all values are strings or string arrays; otherwise `undefined` for raw URL fallback.
107
+ */
108
+ export declare function snapshotSimpleQueryRecord(query: unknown): QueryRecord | undefined;
109
+ /**
110
+ * Clones Node request headers into the framework header record shape.
111
+ *
112
+ * @param headers - Raw Node incoming headers.
113
+ * @returns A shallow header snapshot with array values cloned.
114
+ */
115
+ export declare function cloneRequestHeaders(headers: IncomingHttpHeaders): FrameworkRequest['headers'];
116
+ /**
117
+ * Clones a single Node header value when it is array-backed.
118
+ *
119
+ * @param value - Header value to snapshot.
120
+ * @returns The original scalar value or a cloned array value.
121
+ */
122
+ export declare function cloneHeaderValue<T extends string | string[] | undefined>(value: T): T;
123
+ /**
124
+ * Reads the primary value from a Node header value.
125
+ *
126
+ * @param headerValue - Header value that may contain multiple entries.
127
+ * @returns The first header value when present.
128
+ */
129
+ export declare function readPrimaryHeaderValue(headerValue: string | string[] | undefined): string | undefined;
130
+ /**
131
+ * Normalizes a Node content-type header to its primary media type.
132
+ *
133
+ * @param headerValue - Raw content-type header value.
134
+ * @returns Lowercase primary media type without parameters, or `undefined` when absent.
135
+ */
136
+ export declare function normalizePrimaryContentType(headerValue: string | string[] | undefined): string | undefined;
137
+ /**
138
+ * Parses a Node cookie header into framework cookie values.
139
+ *
140
+ * @param cookieHeader - Raw cookie header value or values.
141
+ * @returns Cookie name/value pairs with percent-decoded values when possible.
142
+ */
143
+ export declare function parseCookieHeader(cookieHeader: string | string[] | undefined): Record<string, string>;
144
+ /**
145
+ * Splits a raw Node request URL into path and search components.
146
+ *
147
+ * @param rawUrl - Raw request URL, absolute URL, or undefined value from Node.
148
+ * @returns The pathname and search string used by framework request matching and query parsing.
149
+ */
150
+ export declare function splitRawRequestUrl(rawUrl: string | undefined): {
151
+ path: string;
152
+ search: string;
153
+ };
154
+ /**
155
+ * Resolves a raw Node request URL into an absolute URL string.
156
+ *
157
+ * @param rawUrl - Raw request URL, absolute URL, or undefined value from Node.
158
+ * @returns An absolute URL suitable for Web-standard parsers.
159
+ */
160
+ export declare function resolveAbsoluteRequestUrl(rawUrl: string | undefined): string;
161
+ export {};
55
162
  //# sourceMappingURL=internal-node-request.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"internal-node-request.d.ts","sourceRoot":"","sources":["../../src/node/internal-node-request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,eAAe,EACf,cAAc,EACf,MAAM,WAAW,CAAC;AAInB,OAAO,EAEL,wBAAwB,EACxB,KAAK,gBAAgB,EACtB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAEL,KAAK,gBAAgB,EAEtB,MAAM,iBAAiB,CAAC;AAUzB;;GAEG;AACH,qBAAa,mCAAoC,SAAQ,wBAAwB;IACnE,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,eAAe;IAIrD,eAAe,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI;CAahD;AAED;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,WAAW,EACnB,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,WAAW,SAAkB,EAC7B,eAAe,UAAQ,GACtB,OAAO,CAAC,gBAAgB,CAAC,CAW3B;AAED;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,WAAW,EACnB,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,WAAW,SAAkB,EAC7B,eAAe,UAAQ,GACtB,gBAAgB,CA2DlB;AAoBD;;;;;GAKG;AACH,wBAAsB,+BAA+B,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9F;AAyBD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,cAAc,GAAG,WAAW,CAezE;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,GAAG,SAAS,CAG5F"}
1
+ {"version":3,"file":"internal-node-request.d.ts","sourceRoot":"","sources":["../../src/node/internal-node-request.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,11 @@ import { Readable } from 'node:stream';
2
2
  import { URL } from 'node:url';
3
3
  import { BadRequestException, PayloadTooLargeException } from '@fluojs/http';
4
4
  import { parseMultipart } from '../multipart.js';
5
+
6
+ /**
7
+ * Options for creating a deferred framework request shell from a Node-backed adapter.
8
+ */
9
+
5
10
  /**
6
11
  * HTTP payload-size error that closes the underlying Node request stream after the response commits.
7
12
  */
@@ -50,21 +55,19 @@ export async function createFrameworkRequest(request, signal, multipartOptions,
50
55
  * @returns The framework request shell with metadata snapshotted and body materialization deferred.
51
56
  */
52
57
  export function createDeferredFrameworkRequest(request, signal, multipartOptions, maxBodySize = 1 * 1024 * 1024, preserveRawBody = false) {
53
- const url = new URL(request.url ?? '/', 'http://localhost');
58
+ const rawUrl = request.url ?? '/';
59
+ const urlParts = splitRawRequestUrl(rawUrl);
54
60
  const headers = cloneRequestHeaders(request.headers);
55
- const cookieHeader = cloneHeaderValue(headers.cookie);
56
- const searchParams = new URLSearchParams(url.searchParams);
57
- const cookies = createMemoizedValue(() => parseCookieHeader(cookieHeader));
58
- const query = createMemoizedValue(() => parseQueryParams(searchParams));
59
61
  const contentType = normalizePrimaryContentType(headers['content-type']);
60
62
  const isMultipart = contentType === 'multipart/form-data';
63
+ let frameworkRequest;
61
64
  const materializeBody = createMemoizedAsyncValue(async () => {
62
65
  if (isMultipart) {
63
66
  const result = await parseMultipart({
64
67
  body: Readable.toWeb(request),
65
68
  headers,
66
69
  method: request.method,
67
- url: url.toString()
70
+ url: resolveAbsoluteRequestUrl(rawUrl)
68
71
  }, {
69
72
  ...multipartOptions,
70
73
  maxTotalSize: multipartOptions?.maxTotalSize ?? maxBodySize
@@ -83,22 +86,85 @@ export function createDeferredFrameworkRequest(request, signal, multipartOptions
83
86
  frameworkRequest.rawBody = bodyResult.rawBody;
84
87
  }
85
88
  });
89
+ frameworkRequest = createDeferredFrameworkRequestShell({
90
+ cookieHeader: cloneHeaderValue(headers.cookie),
91
+ headers,
92
+ materializeBody,
93
+ method: request.method,
94
+ path: urlParts.path,
95
+ queryFactory: () => parseQueryParamsFromSearch(urlParts.search),
96
+ raw: request,
97
+ requestId: resolvePrimaryRequestIdFromHeaders(headers),
98
+ signal,
99
+ url: urlParts.path + urlParts.search
100
+ });
101
+ return frameworkRequest;
102
+ }
103
+
104
+ /**
105
+ * Creates a framework request shell from already-snapshotted Node adapter metadata.
106
+ *
107
+ * @param options - Raw request, metadata factories, and deferred body materialization hooks.
108
+ * @returns A framework request with lazy headers, cookies, query values, and optional body materialization.
109
+ */
110
+ export function createDeferredFrameworkRequestShell({
111
+ cookieHeader,
112
+ headers,
113
+ headersFactory,
114
+ materializeBody,
115
+ method,
116
+ path,
117
+ query,
118
+ queryFactory,
119
+ raw,
120
+ requestId,
121
+ signal,
122
+ url
123
+ }) {
124
+ const hasQuerySnapshot = query !== undefined;
125
+ const hasLazySignal = typeof signal === 'function';
126
+ const resolveHeaders = headersFactory ? createMemoizedValue(headersFactory) : () => headers ?? {};
127
+ const resolveCookies = createMemoizedValue(() => parseCookieHeader(cookieHeader ?? resolveHeaders().cookie));
128
+ const resolveQuery = hasQuerySnapshot ? undefined : createMemoizedValue(() => queryFactory?.() ?? {});
86
129
  const frameworkRequest = {
87
130
  get cookies() {
88
- return cookies();
131
+ return resolveCookies();
89
132
  },
90
- headers,
91
- method: request.method ?? 'GET',
92
- params: {},
93
- path: url.pathname,
94
- get query() {
95
- return query();
133
+ get headers() {
134
+ return resolveHeaders();
96
135
  },
97
- raw: request,
98
- signal,
99
- url: url.pathname + url.search,
100
- materializeBody
136
+ method: method ?? 'GET',
137
+ params: {},
138
+ path,
139
+ requestId,
140
+ raw,
141
+ url
101
142
  };
143
+ if (hasLazySignal) {
144
+ Object.defineProperty(frameworkRequest, 'signal', {
145
+ configurable: true,
146
+ enumerable: true,
147
+ get() {
148
+ return signal();
149
+ }
150
+ });
151
+ } else {
152
+ frameworkRequest.signal = signal;
153
+ }
154
+ if (hasQuerySnapshot) {
155
+ frameworkRequest.query = query;
156
+ } else {
157
+ Object.defineProperty(frameworkRequest, 'query', {
158
+ configurable: true,
159
+ enumerable: true,
160
+ get() {
161
+ return resolveQuery();
162
+ }
163
+ });
164
+ }
165
+ if (materializeBody) {
166
+ frameworkRequest.materializeBody = materializeBody;
167
+ }
102
168
  return frameworkRequest;
103
169
  }
104
170
  function hasNodeRequestBody(request) {
@@ -125,7 +191,14 @@ export async function materializeFrameworkRequestBody(request) {
125
191
  await request.materializeBody?.();
126
192
  delete request.materializeBody;
127
193
  }
128
- function createMemoizedValue(factory) {
194
+
195
+ /**
196
+ * Creates a synchronous memoized value resolver.
197
+ *
198
+ * @param factory - Function that computes the value on first access.
199
+ * @returns A stable resolver that returns the cached value after the first call.
200
+ */
201
+ export function createMemoizedValue(factory) {
129
202
  let initialized = false;
130
203
  let value;
131
204
  return () => {
@@ -136,7 +209,14 @@ function createMemoizedValue(factory) {
136
209
  return value;
137
210
  };
138
211
  }
139
- function createMemoizedAsyncValue(factory) {
212
+
213
+ /**
214
+ * Creates an async memoized side-effect resolver.
215
+ *
216
+ * @param factory - Async function to run at most once.
217
+ * @returns A resolver that returns the same in-flight or completed promise for every call.
218
+ */
219
+ export function createMemoizedAsyncValue(factory) {
140
220
  let promise;
141
221
  return () => {
142
222
  promise ??= factory();
@@ -175,6 +255,20 @@ export function resolveRequestIdFromHeaders(headers) {
175
255
  const requestId = headers['x-request-id'] ?? headers['x-correlation-id'];
176
256
  return Array.isArray(requestId) ? requestId[0] : requestId;
177
257
  }
258
+ function resolvePrimaryRequestIdFromHeaders(headers) {
259
+ const requestId = headers['x-request-id'];
260
+ return Array.isArray(requestId) ? requestId[0] : requestId;
261
+ }
262
+
263
+ /**
264
+ * Parses a raw URL search string into the framework query shape.
265
+ *
266
+ * @param search - Raw search string, with or without a leading question mark.
267
+ * @returns Query values where repeated keys become string arrays.
268
+ */
269
+ export function parseQueryParamsFromSearch(search) {
270
+ return parseQueryParams(new URLSearchParams(search));
271
+ }
178
272
  function parseQueryParams(searchParams) {
179
273
  const query = {};
180
274
  for (const [key, value] of searchParams.entries()) {
@@ -191,20 +285,73 @@ function parseQueryParams(searchParams) {
191
285
  }
192
286
  return query;
193
287
  }
194
- function cloneRequestHeaders(headers) {
288
+
289
+ /**
290
+ * Snapshots host-parsed query values when they already match framework semantics.
291
+ *
292
+ * @param query - Host query object exposed by a Node-backed adapter.
293
+ * @returns A cloned query record when all values are strings or string arrays; otherwise `undefined` for raw URL fallback.
294
+ */
295
+ export function snapshotSimpleQueryRecord(query) {
296
+ if (typeof query !== 'object' || query === null) {
297
+ return undefined;
298
+ }
299
+ const snapshot = {};
300
+ for (const [key, value] of Object.entries(query)) {
301
+ if (typeof value === 'string') {
302
+ snapshot[key] = value;
303
+ continue;
304
+ }
305
+ if (Array.isArray(value) && value.every(item => typeof item === 'string')) {
306
+ snapshot[key] = [...value];
307
+ continue;
308
+ }
309
+ return undefined;
310
+ }
311
+ return snapshot;
312
+ }
313
+
314
+ /**
315
+ * Clones Node request headers into the framework header record shape.
316
+ *
317
+ * @param headers - Raw Node incoming headers.
318
+ * @returns A shallow header snapshot with array values cloned.
319
+ */
320
+ export function cloneRequestHeaders(headers) {
195
321
  const clonedEntries = Object.entries(headers).map(([name, value]) => [name, cloneHeaderValue(value)]);
196
322
  return Object.fromEntries(clonedEntries);
197
323
  }
198
- function cloneHeaderValue(value) {
324
+
325
+ /**
326
+ * Clones a single Node header value when it is array-backed.
327
+ *
328
+ * @param value - Header value to snapshot.
329
+ * @returns The original scalar value or a cloned array value.
330
+ */
331
+ export function cloneHeaderValue(value) {
199
332
  return Array.isArray(value) ? [...value] : value;
200
333
  }
201
- function readPrimaryHeaderValue(headerValue) {
334
+
335
+ /**
336
+ * Reads the primary value from a Node header value.
337
+ *
338
+ * @param headerValue - Header value that may contain multiple entries.
339
+ * @returns The first header value when present.
340
+ */
341
+ export function readPrimaryHeaderValue(headerValue) {
202
342
  if (Array.isArray(headerValue)) {
203
343
  return headerValue[0];
204
344
  }
205
345
  return headerValue;
206
346
  }
207
- function normalizePrimaryContentType(headerValue) {
347
+
348
+ /**
349
+ * Normalizes a Node content-type header to its primary media type.
350
+ *
351
+ * @param headerValue - Raw content-type header value.
352
+ * @returns Lowercase primary media type without parameters, or `undefined` when absent.
353
+ */
354
+ export function normalizePrimaryContentType(headerValue) {
208
355
  const primaryHeaderValue = readPrimaryHeaderValue(headerValue);
209
356
  if (typeof primaryHeaderValue !== 'string') {
210
357
  return undefined;
@@ -220,7 +367,14 @@ function decodeCookieValue(raw) {
220
367
  return raw;
221
368
  }
222
369
  }
223
- function parseCookieHeader(cookieHeader) {
370
+
371
+ /**
372
+ * Parses a Node cookie header into framework cookie values.
373
+ *
374
+ * @param cookieHeader - Raw cookie header value or values.
375
+ * @returns Cookie name/value pairs with percent-decoded values when possible.
376
+ */
377
+ export function parseCookieHeader(cookieHeader) {
224
378
  const normalizedCookieHeader = Array.isArray(cookieHeader) ? cookieHeader.join('; ') : cookieHeader;
225
379
  if (!normalizedCookieHeader) {
226
380
  return {};
@@ -272,4 +426,51 @@ async function readRequestBody(request, contentType, maxBodySize = 1 * 1024 * 10
272
426
  body: bodyText,
273
427
  rawBody: preserveRawBody ? rawBody : undefined
274
428
  };
429
+ }
430
+
431
+ /**
432
+ * Splits a raw Node request URL into path and search components.
433
+ *
434
+ * @param rawUrl - Raw request URL, absolute URL, or undefined value from Node.
435
+ * @returns The pathname and search string used by framework request matching and query parsing.
436
+ */
437
+ export function splitRawRequestUrl(rawUrl) {
438
+ const resolvedRawUrl = rawUrl ?? '/';
439
+ if (resolvedRawUrl.startsWith('http://') || resolvedRawUrl.startsWith('https://')) {
440
+ const url = new URL(resolvedRawUrl);
441
+ return {
442
+ path: url.pathname,
443
+ search: url.search
444
+ };
445
+ }
446
+ const queryStart = resolvedRawUrl.indexOf('?');
447
+ const hashStart = resolvedRawUrl.indexOf('#');
448
+ const pathEndCandidates = [queryStart, hashStart].filter(index => index >= 0);
449
+ const pathEnd = pathEndCandidates.length > 0 ? Math.min(...pathEndCandidates) : resolvedRawUrl.length;
450
+ const path = resolvedRawUrl.slice(0, pathEnd) || '/';
451
+ if (queryStart === -1) {
452
+ return {
453
+ path,
454
+ search: ''
455
+ };
456
+ }
457
+ const searchEnd = hashStart >= 0 && hashStart > queryStart ? hashStart : resolvedRawUrl.length;
458
+ return {
459
+ path,
460
+ search: resolvedRawUrl.slice(queryStart, searchEnd)
461
+ };
462
+ }
463
+
464
+ /**
465
+ * Resolves a raw Node request URL into an absolute URL string.
466
+ *
467
+ * @param rawUrl - Raw request URL, absolute URL, or undefined value from Node.
468
+ * @returns An absolute URL suitable for Web-standard parsers.
469
+ */
470
+ export function resolveAbsoluteRequestUrl(rawUrl) {
471
+ const resolvedRawUrl = rawUrl ?? '/';
472
+ if (resolvedRawUrl.startsWith('http://') || resolvedRawUrl.startsWith('https://')) {
473
+ return resolvedRawUrl;
474
+ }
475
+ return new URL(resolvedRawUrl, 'http://localhost').toString();
275
476
  }
@@ -2,6 +2,7 @@ import { createServer as createHttpServer } from 'node:http';
2
2
  import { createServer as createHttpsServer, type ServerOptions as HttpsServerOptions } from 'node:https';
3
3
  import { type CorsOptions, type Dispatcher, type HttpApplicationAdapter, type MiddlewareLike, type SecurityHeadersOptions } from '@fluojs/http';
4
4
  import { createNodeResponseCompression, compressNodeResponse } from './internal-node-compression.js';
5
+ import { cloneHeaderValue, cloneRequestHeaders, createDeferredFrameworkRequestShell, createMemoizedAsyncValue, createMemoizedValue, normalizePrimaryContentType, parseCookieHeader, parseQueryParamsFromSearch, createRequestSignal, readPrimaryHeaderValue, resolveAbsoluteRequestUrl, resolveRequestIdFromHeaders, snapshotSimpleQueryRecord, splitRawRequestUrl } from './internal-node-request.js';
5
6
  import { createNodeShutdownSignalRegistration, defaultNodeShutdownSignals, registerShutdownSignals } from './internal-node-shutdown.js';
6
7
  import type { MultipartOptions, UploadedFile } from '../multipart.js';
7
8
  import type { Application, ApplicationLogger, CreateApplicationOptions, ModuleType } from '../types.js';
@@ -112,5 +113,5 @@ export declare function bootstrapNodeApplication(rootModule: ModuleType, options
112
113
  * @returns The run node application result.
113
114
  */
114
115
  export declare function runNodeApplication(rootModule: ModuleType, options: RunNodeApplicationOptions): Promise<Application>;
115
- export { compressNodeResponse, createNodeResponseCompression, createNodeShutdownSignalRegistration, defaultNodeShutdownSignals, registerShutdownSignals, };
116
+ export { cloneHeaderValue, cloneRequestHeaders, compressNodeResponse, createDeferredFrameworkRequestShell, createMemoizedAsyncValue, createMemoizedValue, createRequestSignal, createNodeResponseCompression, createNodeShutdownSignalRegistration, defaultNodeShutdownSignals, normalizePrimaryContentType, parseCookieHeader, parseQueryParamsFromSearch, readPrimaryHeaderValue, registerShutdownSignals, resolveAbsoluteRequestUrl, resolveRequestIdFromHeaders, snapshotSimpleQueryRecord, splitRawRequestUrl, };
116
117
  //# sourceMappingURL=internal-node.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"internal-node.d.ts","sourceRoot":"","sources":["../../src/node/internal-node.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"internal-node.d.ts","sourceRoot":"","sources":["../../src/node/internal-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,IAAI,gBAAgB,EAA6C,MAAM,WAAW,CAAC;AACxG,OAAO,EAAE,YAAY,IAAI,iBAAiB,EAAE,KAAK,aAAa,IAAI,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGzG,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,sBAAsB,EAC3B,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC5B,MAAM,cAAc,CAAC;AAMtB,OAAO,EACL,6BAA6B,EAC7B,oBAAoB,EACrB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,mCAAmC,EAEnC,wBAAwB,EACxB,mBAAmB,EAEnB,2BAA2B,EAC3B,iBAAiB,EACjB,0BAA0B,EAC1B,mBAAmB,EAEnB,sBAAsB,EACtB,yBAAyB,EACzB,2BAA2B,EAC3B,yBAAyB,EACzB,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AAMpC,OAAO,EACL,oCAAoC,EACpC,0BAA0B,EAC1B,uBAAuB,EACxB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAKtE,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAExG,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,gBAAgB;QACxB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;QACvB,OAAO,CAAC,EAAE,UAAU,CAAC;KACtB;CACF;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEzD;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,WAAW,CAAC;AAIhE;;GAEG;AACH,MAAM,WAAW,+BAAgC,SAAQ,IAAI,CAAC,wBAAwB,EAAE,SAAS,GAAG,QAAQ,GAAG,YAAY,CAAC;IAC1H,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,KAAK,GAAG,sBAAsB,CAAC;IACjD,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,yBAA0B,SAAQ,+BAA+B;IAChF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,KAAK,GAAG,SAAS,qBAAqB,EAAE,CAAC;CAC5D;AAED,UAAU,gBAAgB;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACb;AASD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAG7F;;GAEG;AACH,qBAAa,0BAA2B,YAAW,sBAAsB;IAWrE,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAE3B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAI7B,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAnBpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAIrC;IACF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAG1B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,YAAY,oBAAM,EAClB,UAAU,oBAAK,EAChC,WAAW,qBAAQ,EACF,YAAY,EAAE,kBAAkB,GAAG,SAAS,EAC7D,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,WAAW,SAAkB,EAC7B,eAAe,UAAQ,EACN,iBAAiB,SAA8B;IAmBlE,SAAS,IAAI,UAAU;IAIvB,qBAAqB;IAIrB,eAAe,IAAI,gBAAgB;IAI7B,MAAM,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAU7C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAad,aAAa;CAY5B;AAiDD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,EAAE,WAAW,UAAQ,EAAE,gBAAgB,CAAC,EAAE,gBAAgB,GAAG,sBAAsB,CAa5J;AAED;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAC5C,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,WAAW,CAAC,CAMtB;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,WAAW,CAAC,CAQtB;AAED,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,oBAAoB,EACpB,mCAAmC,EACnC,wBAAwB,EACxB,mBAAmB,EACnB,mBAAmB,EACnB,6BAA6B,EAC7B,oCAAoC,EACpC,0BAA0B,EAC1B,2BAA2B,EAC3B,iBAAiB,EACjB,0BAA0B,EAC1B,sBAAsB,EACtB,uBAAuB,EACvB,yBAAyB,EACzB,2BAA2B,EAC3B,yBAAyB,EACzB,kBAAkB,GACnB,CAAC"}
@@ -3,7 +3,7 @@ import { createServer as createHttpsServer } from 'node:https';
3
3
  import { createServerBackedHttpAdapterRealtimeCapability } from '@fluojs/http';
4
4
  import { bootstrapHttpAdapterApplication, runHttpAdapterApplication } from '../http-adapter-shared.js';
5
5
  import { createNodeResponseCompression, compressNodeResponse } from './internal-node-compression.js';
6
- import { createDeferredFrameworkRequest, NodeRequestPayloadTooLargeException, createRequestSignal, materializeFrameworkRequestBody, resolveRequestIdFromHeaders } from './internal-node-request.js';
6
+ import { cloneHeaderValue, cloneRequestHeaders, createDeferredFrameworkRequestShell, createDeferredFrameworkRequest, createMemoizedAsyncValue, createMemoizedValue, NodeRequestPayloadTooLargeException, normalizePrimaryContentType, parseCookieHeader, parseQueryParamsFromSearch, createRequestSignal, materializeFrameworkRequestBody, readPrimaryHeaderValue, resolveAbsoluteRequestUrl, resolveRequestIdFromHeaders, snapshotSimpleQueryRecord, splitRawRequestUrl } from './internal-node-request.js';
7
7
  import { createFrameworkResponse, writeNodeAdapterErrorResponse } from './internal-node-response.js';
8
8
  import { createNodeShutdownSignalRegistration, defaultNodeShutdownSignals, registerShutdownSignals } from './internal-node-shutdown.js';
9
9
  import { dispatchWithRequestResponseFactory } from '../adapters/request-response-factory.js';
@@ -156,7 +156,7 @@ export async function runNodeApplication(rootModule, options) {
156
156
  shutdownRegistration: createNodeShutdownSignalRegistration(options.shutdownSignals ?? defaultNodeShutdownSignals())
157
157
  }, adapter);
158
158
  }
159
- export { compressNodeResponse, createNodeResponseCompression, createNodeShutdownSignalRegistration, defaultNodeShutdownSignals, registerShutdownSignals };
159
+ export { cloneHeaderValue, cloneRequestHeaders, compressNodeResponse, createDeferredFrameworkRequestShell, createMemoizedAsyncValue, createMemoizedValue, createRequestSignal, createNodeResponseCompression, createNodeShutdownSignalRegistration, defaultNodeShutdownSignals, normalizePrimaryContentType, parseCookieHeader, parseQueryParamsFromSearch, readPrimaryHeaderValue, registerShutdownSignals, resolveAbsoluteRequestUrl, resolveRequestIdFromHeaders, snapshotSimpleQueryRecord, splitRawRequestUrl };
160
160
  function createNodeServer(httpsOptions, handler) {
161
161
  return httpsOptions ? createHttpsServer(httpsOptions, handler) : createHttpServer(handler);
162
162
  }
package/dist/web.d.ts CHANGED
@@ -11,8 +11,10 @@ declare module '@fluojs/http' {
11
11
  * Configures Web request parsing, multipart handling, and raw body preservation.
12
12
  */
13
13
  export interface CreateWebRequestResponseFactoryOptions {
14
+ consumeOriginalBody?: boolean;
14
15
  maxBodySize?: number;
15
16
  multipart?: MultipartOptions;
17
+ preferNativeJsonBodyReader?: boolean;
16
18
  rawBody?: boolean;
17
19
  }
18
20
  /**
package/dist/web.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,cAAc,CAAC;AAMtB,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,wCAAwC,CAAC;AAEhD,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,gBAAgB;QACxB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;QACvB,OAAO,CAAC,EAAE,UAAU,CAAC;KACtB;CACF;AAOD;;GAEG;AACH,MAAM,WAAW,sCAAsC;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,yBAA0B,SAAQ,sCAAsC;IACvF,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,EAAE,oBAAoB,CAAC,CAAC;IACzF,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC7D,UAAU,IAAI,QAAQ,CAAC;CACxB;AAED,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAiNhD;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,sCAA2C,GACnD,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,EAAE,oBAAoB,CAAC,CA6BhF;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,EACvC,UAAU,EACV,yBAAiG,EACjG,OAAO,EACP,OAAO,EACP,GAAG,OAAO,EACX,EAAE,yBAAyB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAU/C;AAED;;;;;;;;;GASG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,WAAW,EACnB,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,WAAW,SAAwB,EACnC,eAAe,UAAQ,GACtB,OAAO,CAAC,gBAAgB,CAAC,CAW3B"}
1
+ {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,cAAc,CAAC;AAMtB,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,wCAAwC,CAAC;AAEhD,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,gBAAgB;QACxB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;QACvB,OAAO,CAAC,EAAE,UAAU,CAAC;KACtB;CACF;AAOD;;GAEG;AACH,MAAM,WAAW,sCAAsC;IACrD,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,yBAA0B,SAAQ,sCAAsC;IACvF,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,EAAE,oBAAoB,CAAC,CAAC;IACzF,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC7D,UAAU,IAAI,QAAQ,CAAC;CACxB;AAED,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAgNhD;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,sCAA2C,GACnD,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,EAAE,oBAAoB,CAAC,CA+BhF;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,EACvC,UAAU,EACV,yBAAiG,EACjG,OAAO,EACP,OAAO,EACP,GAAG,OAAO,EACX,EAAE,yBAAyB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAU/C;AAED;;;;;;;;;GASG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,WAAW,EACnB,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,WAAW,SAAwB,EACnC,eAAe,UAAQ,GACtB,OAAO,CAAC,gBAAgB,CAAC,CAW3B"}
package/dist/web.js CHANGED
@@ -146,8 +146,7 @@ class MutableWebFrameworkResponse {
146
146
  headers: toResponseHeaders(this.headers),
147
147
  status: this.statusCode ?? 200
148
148
  };
149
- const responseBody = this.responseBody instanceof Uint8Array ? this.responseBody.slice().buffer : this.responseBody;
150
- const nativeResponseBody = isResponseBodyForbidden(init.status) ? undefined : responseBody ?? '';
149
+ const nativeResponseBody = isResponseBodyForbidden(init.status) ? undefined : this.responseBody === undefined ? '' : this.responseBody;
151
150
  this.finalizedResponse = this.streamActive ? new Response(this.getOrCreateResponseStream().readable, init) : new Response(nativeResponseBody, init);
152
151
  this.raw = this.finalizedResponse;
153
152
  this.committed = true;
@@ -171,7 +170,7 @@ class MutableWebFrameworkResponse {
171
170
  export function createWebRequestResponseFactory(options = {}) {
172
171
  return {
173
172
  async createRequest(request, signal) {
174
- return createDeferredWebFrameworkRequest(request, signal, options.multipart, options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE, options.rawBody ?? false);
173
+ return createDeferredWebFrameworkRequest(request, signal, options.multipart, options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE, options.rawBody ?? false, options.preferNativeJsonBodyReader ?? false, options.consumeOriginalBody ?? false);
175
174
  },
176
175
  materializeRequest(request) {
177
176
  return materializeWebFrameworkRequestBody(request);
@@ -242,26 +241,20 @@ export async function createWebFrameworkRequest(request, signal, multipartOption
242
241
  * @param preserveRawBody - Whether materialization should retain raw request body bytes.
243
242
  * @returns The framework request shell with metadata snapshotted and body materialization deferred.
244
243
  */
245
- function createDeferredWebFrameworkRequest(request, signal, multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false) {
244
+ function createDeferredWebFrameworkRequest(request, signal, multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false, preferNativeJsonBodyReader = false, consumeOriginalBody = false) {
246
245
  const url = new URL(request.url);
247
- const headerEntries = Array.from(request.headers.entries());
246
+ const requestHeaders = new Headers(request.headers);
248
247
  const method = request.method;
249
- const cookieHeader = request.headers.get('cookie') ?? undefined;
250
- const searchParams = new URLSearchParams(url.searchParams);
251
- const headers = createMemoizedValue(() => cloneWebHeaders(headerEntries));
252
- const cookies = createMemoizedValue(() => parseCookieHeader(cookieHeader));
253
- const query = createMemoizedValue(() => parseQueryParams(searchParams));
254
- const contentType = request.headers.get('content-type') ?? undefined;
248
+ const headers = createMemoizedValue(() => cloneWebHeaders(requestHeaders));
249
+ const cookies = createMemoizedValue(() => parseCookieHeader(requestHeaders.get('cookie') ?? undefined));
250
+ const query = createMemoizedValue(() => parseQueryString(url.search));
251
+ const contentType = requestHeaders.get('content-type') ?? undefined;
255
252
  const isMultipart = typeof contentType === 'string' && contentType.includes('multipart/form-data');
256
- const materializeBody = createMemoizedAsyncValue(async () => {
253
+ const hasRequestBody = request.body !== null;
254
+ const materializeBody = hasRequestBody ? createMemoizedAsyncValue(async () => {
257
255
  if (isMultipart) {
258
256
  const materializedRequest = request.clone();
259
- const result = await parseMultipart({
260
- body: materializedRequest.body,
261
- headers: new Headers(headerEntries),
262
- method,
263
- url: url.toString()
264
- }, {
257
+ const result = await parseMultipart(createRequestWithSnapshotMetadata(materializedRequest, request.url, method, requestHeaders), {
265
258
  ...multipartOptions,
266
259
  maxTotalSize: multipartOptions?.maxTotalSize ?? maxBodySize
267
260
  });
@@ -274,12 +267,13 @@ function createDeferredWebFrameworkRequest(request, signal, multipartOptions, ma
274
267
  frameworkRequest.body = undefined;
275
268
  return;
276
269
  }
277
- const bodyResult = await readWebRequestBody(request.clone(), contentType, maxBodySize, preserveRawBody);
270
+ const requestToRead = consumeOriginalBody ? request : request.clone();
271
+ const bodyResult = await readWebRequestBody(requestToRead, contentType, maxBodySize, preserveRawBody, preferNativeJsonBodyReader);
278
272
  frameworkRequest.body = bodyResult.body;
279
273
  if (bodyResult.rawBody) {
280
274
  frameworkRequest.rawBody = bodyResult.rawBody;
281
275
  }
282
- });
276
+ }) : undefined;
283
277
  const frameworkRequest = {
284
278
  get cookies() {
285
279
  return cookies();
@@ -294,13 +288,28 @@ function createDeferredWebFrameworkRequest(request, signal, multipartOptions, ma
294
288
  return query();
295
289
  },
296
290
  raw: request,
291
+ requestId: requestHeaders.get('x-request-id') ?? undefined,
297
292
  signal,
298
293
  url: url.pathname + url.search,
299
294
  materializeBody
300
295
  };
296
+ if (!hasRequestBody) {
297
+ frameworkRequest.body = undefined;
298
+ }
301
299
  const nativeRouteHandoff = consumeRawRequestNativeRouteHandoff(request);
302
300
  return nativeRouteHandoff ? attachFrameworkRequestNativeRouteHandoff(frameworkRequest, nativeRouteHandoff) : frameworkRequest;
303
301
  }
302
+ function createRequestWithSnapshotMetadata(request, url, method, headers) {
303
+ const init = {
304
+ headers: new Headers(headers),
305
+ method
306
+ };
307
+ if (request.body) {
308
+ init.body = request.body;
309
+ init.duplex = 'half';
310
+ }
311
+ return new Request(url, init);
312
+ }
304
313
  function validateWebRequestContentLength(request, maxBodySize) {
305
314
  const contentLength = request.headers.get('content-length');
306
315
  if (contentLength === null) {
@@ -340,25 +349,56 @@ function createMemoizedAsyncValue(factory) {
340
349
  return promise;
341
350
  };
342
351
  }
343
- function parseQueryParams(searchParams) {
352
+ function parseQueryString(search) {
344
353
  const query = {};
345
- for (const [key, value] of searchParams.entries()) {
346
- const current = query[key];
347
- if (current === undefined) {
348
- query[key] = value;
349
- continue;
354
+ if (search.length <= 1) {
355
+ return query;
356
+ }
357
+ let index = search.charCodeAt(0) === 63 ? 1 : 0;
358
+ while (index <= search.length) {
359
+ let nextDelimiter = search.indexOf('&', index);
360
+ if (nextDelimiter === -1) {
361
+ nextDelimiter = search.length;
350
362
  }
351
- if (Array.isArray(current)) {
352
- current.push(value);
353
- continue;
363
+ const entry = search.slice(index, nextDelimiter);
364
+ if (entry.length > 0) {
365
+ const separatorIndex = entry.indexOf('=');
366
+ const rawKey = separatorIndex === -1 ? entry : entry.slice(0, separatorIndex);
367
+ const rawValue = separatorIndex === -1 ? '' : entry.slice(separatorIndex + 1);
368
+ const key = decodeQueryComponent(rawKey, 'key');
369
+ const value = decodeQueryComponent(rawValue, 'value');
370
+ const current = query[key];
371
+ if (current === undefined) {
372
+ query[key] = value;
373
+ } else if (Array.isArray(current)) {
374
+ current.push(value);
375
+ } else {
376
+ query[key] = [current, value];
377
+ }
354
378
  }
355
- query[key] = [current, value];
379
+ index = nextDelimiter + 1;
356
380
  }
357
381
  return query;
358
382
  }
383
+ function decodeQueryComponent(value, kind) {
384
+ const normalizedValue = value.includes('+') ? value.replaceAll('+', ' ') : value;
385
+ try {
386
+ return decodeURIComponent(normalizedValue);
387
+ } catch {
388
+ return decodeQueryComponentLikeUrlSearchParams(value, kind);
389
+ }
390
+ }
391
+ function decodeQueryComponentLikeUrlSearchParams(value, kind) {
392
+ if (kind === 'key') {
393
+ const params = new URLSearchParams(`${value}=`);
394
+ return params.keys().next().value ?? '';
395
+ }
396
+ const params = new URLSearchParams(`x=${value}`);
397
+ return params.get('x') ?? '';
398
+ }
359
399
  function cloneWebHeaders(headers) {
360
400
  const clonedHeaders = {};
361
- for (const [name, value] of headers) {
401
+ for (const [name, value] of headers.entries()) {
362
402
  clonedHeaders[name] = value;
363
403
  }
364
404
  return clonedHeaders;
@@ -382,14 +422,23 @@ function parseCookieHeader(cookieHeader) {
382
422
  return [pair.slice(0, index).trim(), decodeCookieValue(pair.slice(index + 1).trim())];
383
423
  }));
384
424
  }
385
- async function readWebRequestBody(request, contentType, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false) {
425
+ async function readWebRequestBody(request, contentType, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false, preferNativeJsonBodyReader = false) {
386
426
  validateWebRequestContentLength(request, maxBodySize);
387
427
  if (!request.body) {
388
428
  return {
389
429
  body: undefined
390
430
  };
391
431
  }
392
- const rawBody = await readByteLimitedStream(request.body, maxBodySize);
432
+ if (!preserveRawBody && isJsonContentType(contentType) && (preferNativeJsonBodyReader || isContentLengthWithinLimit(request, maxBodySize))) {
433
+ const rawBody = new Uint8Array(await request.arrayBuffer());
434
+ if (rawBody.byteLength > maxBodySize) {
435
+ throw new PayloadTooLargeException(REQUEST_BODY_LIMIT_MESSAGE);
436
+ }
437
+ return parseWebRequestRawBody(rawBody, contentType, preserveRawBody);
438
+ }
439
+ return parseWebRequestRawBody(await readByteLimitedStream(request.body, maxBodySize), contentType, preserveRawBody);
440
+ }
441
+ function parseWebRequestRawBody(rawBody, contentType, preserveRawBody) {
393
442
  if (rawBody.byteLength === 0) {
394
443
  return {
395
444
  body: undefined
@@ -402,7 +451,7 @@ async function readWebRequestBody(request, contentType, maxBodySize = DEFAULT_MA
402
451
  rawBody: preserveRawBody ? rawBody : undefined
403
452
  };
404
453
  }
405
- if (typeof contentType === 'string' && contentType.includes('application/json')) {
454
+ if (isJsonContentType(contentType)) {
406
455
  try {
407
456
  return {
408
457
  body: JSON.parse(bodyText),
@@ -417,6 +466,14 @@ async function readWebRequestBody(request, contentType, maxBodySize = DEFAULT_MA
417
466
  rawBody: preserveRawBody ? rawBody : undefined
418
467
  };
419
468
  }
469
+ function isContentLengthWithinLimit(request, maxBodySize) {
470
+ const contentLength = request.headers.get('content-length');
471
+ if (contentLength === null) {
472
+ return false;
473
+ }
474
+ const parsedContentLength = Number(contentLength);
475
+ return Number.isFinite(parsedContentLength) && parsedContentLength > 0 && parsedContentLength <= maxBodySize;
476
+ }
420
477
  async function readByteLimitedStream(stream, maxBodySize) {
421
478
  const reader = stream.getReader();
422
479
  const chunks = [];
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "module-graph",
10
10
  "orchestration"
11
11
  ],
12
- "version": "1.0.0-beta.6",
12
+ "version": "1.0.0-beta.8",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -70,7 +70,7 @@
70
70
  "@fluojs/config": "^1.0.0-beta.3",
71
71
  "@fluojs/core": "^1.0.0-beta.2",
72
72
  "@fluojs/di": "^1.0.0-beta.5",
73
- "@fluojs/http": "^1.0.0-beta.4"
73
+ "@fluojs/http": "^1.0.0-beta.6"
74
74
  },
75
75
  "devDependencies": {
76
76
  "vitest": "^3.2.4",