@fluojs/platform-express 1.0.0-beta.3 → 1.0.0-beta.5
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 +7 -2
- package/README.md +6 -2
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +121 -108
- package/dist/testing-http-adapter-portability.d.js +0 -0
- package/package.json +5 -4
package/README.ko.md
CHANGED
|
@@ -44,6 +44,8 @@ await app.listen();
|
|
|
44
44
|
### 스트리밍 응답 처리 (SSE)
|
|
45
45
|
Express 어댑터는 공유 `SseResponse` 유틸리티를 통해 Server-Sent Events(SSE)를 지원하며, Express 전용 스트림 처리를 추상화합니다.
|
|
46
46
|
|
|
47
|
+
Express 기반 응답 스트림은 공유 fluo 백프레셔 계약도 따릅니다. `response.stream.waitForDrain()`은 `drain`, `close`, `error` 중 어느 쪽이 먼저 와도 완료되므로, 백프레셔가 풀리기 전에 클라이언트가 연결을 끊어도 스트리밍 작성기가 멈추지 않습니다.
|
|
48
|
+
|
|
47
49
|
```typescript
|
|
48
50
|
@Get('events')
|
|
49
51
|
async streamEvents(@Res() res: FrameworkResponse) {
|
|
@@ -71,9 +73,11 @@ const adapter = createExpressAdapter(
|
|
|
71
73
|
### 안전한 fallback을 포함한 Native Route Registration
|
|
72
74
|
이제 어댑터는 의미 보존이 가능한 명시적 HTTP 메서드 라우트를 Express Router에 사전 등록하면서도, 실제 요청 처리는 계속 공유 fluo dispatcher를 통해 수행합니다.
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
의미 보존이 가능한 unversioned route에서는 Express가 미리 고른 descriptor와 params를 공유 dispatcher에 전달하므로 duplicate route matching을 건너뛰면서도 guards, interceptors, observers, body parsing, raw body 캡처, SSE, 오류 응답은 기존과 같은 framework-owned 실행 경로를 유지합니다.
|
|
77
|
+
|
|
78
|
+
어댑터가 native handoff를 붙인 뒤 app middleware가 framework request의 method 또는 path를 rewrite하면 dispatcher는 그 handoff를 stale로 보고 원래 Express match를 재사용하지 않고 rewrite된 요청을 다시 매칭합니다.
|
|
75
79
|
|
|
76
|
-
문서화된 fluo semantics를 바꾸지 않기 위해 `/:id` 와 `/:slug`처럼 shape가 겹치는 파라미터 라우트, `@All(...)` 핸들러, `OPTIONS` 소유권, 그리고 duplicate slash/trailing slash 정규화에 의존하는 요청은 catch-all fallback 경로에 남겨둡니다.
|
|
80
|
+
문서화된 fluo semantics를 바꾸지 않기 위해 `/:id` 와 `/:slug`처럼 shape가 겹치는 파라미터 라우트, `@All(...)` 핸들러, `OPTIONS` 소유권, non-URI versioning, 그리고 duplicate slash/trailing slash 정규화에 의존하는 요청은 catch-all fallback 경로에 남겨둡니다.
|
|
77
81
|
|
|
78
82
|
## 어댑터 계약
|
|
79
83
|
|
|
@@ -82,6 +86,7 @@ const adapter = createExpressAdapter(
|
|
|
82
86
|
- **OPTIONS 소유권 parity**: 어댑터는 native route에 대해 Express Router가 `OPTIONS`를 자동 응답하지 못하게 막아, 미지원 메서드도 계속 fluo dispatcher semantics로 흘러가고 `@All(...)` 핸들러가 정의된 경우 `OPTIONS`도 그대로 소유할 수 있게 합니다.
|
|
83
87
|
- **경로 정규화 parity**: duplicate slash 변형처럼 Express Router와 fluo의 정규화 방식이 다를 수 있는 요청도 fallback dispatch를 통해 fluo의 normalized route contract를 유지합니다.
|
|
84
88
|
- **버저닝 parity**: Express Router가 최초 path match를 하더라도 header/media-type/custom version 선택은 계속 dispatcher가 최종 결정합니다.
|
|
89
|
+
- **Middleware rewrite parity**: App middleware가 method/path를 rewrite하면 native handoff는 무효화되고 rewrite된 요청을 기준으로 다시 매칭합니다.
|
|
85
90
|
|
|
86
91
|
## 공개 API 개요
|
|
87
92
|
|
package/README.md
CHANGED
|
@@ -44,6 +44,8 @@ await app.listen();
|
|
|
44
44
|
### Handling Streaming Responses (SSE)
|
|
45
45
|
The Express adapter supports Server-Sent Events (SSE) via the shared `SseResponse` utility, abstracting away the Express-specific stream handling.
|
|
46
46
|
|
|
47
|
+
Express-backed response streams also honor the shared fluo backpressure contract: `response.stream.waitForDrain()` settles on `drain`, `close`, or `error`, so streaming writers do not hang when clients disconnect before backpressure clears.
|
|
48
|
+
|
|
47
49
|
```typescript
|
|
48
50
|
@Get('events')
|
|
49
51
|
async streamEvents(@Res() res: FrameworkResponse) {
|
|
@@ -71,9 +73,11 @@ const adapter = createExpressAdapter(
|
|
|
71
73
|
### Native Route Registration with Safe Fallback
|
|
72
74
|
The adapter now pre-registers semantically safe Express Router handlers for explicit HTTP methods and still dispatches those requests through the shared fluo dispatcher.
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
For semantically safe unversioned routes, Express hands the pre-matched descriptor and params to the shared dispatcher so duplicate route matching is skipped while guards, interceptors, observers, body parsing, raw body capture, SSE, and error responses stay on the same framework-owned execution path.
|
|
77
|
+
|
|
78
|
+
If app middleware rewrites the framework request method or path after the adapter attaches a native handoff, the dispatcher treats that handoff as stale and rematches the rewritten request instead of reusing the original Express match.
|
|
75
79
|
|
|
76
|
-
To avoid changing documented fluo semantics, overlapping same-shape param routes such as `/:id` and `/:slug`, `@All(...)` handlers, `OPTIONS` ownership, and requests that rely on fluo's duplicate-slash/trailing-slash normalization stay on the catch-all fallback path.
|
|
80
|
+
To avoid changing documented fluo semantics, overlapping same-shape param routes such as `/:id` and `/:slug`, `@All(...)` handlers, `OPTIONS` ownership, non-URI versioning, and requests that rely on fluo's duplicate-slash/trailing-slash normalization stay on the catch-all fallback path.
|
|
77
81
|
|
|
78
82
|
## Adapter Contract
|
|
79
83
|
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
package/dist/adapter.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createServer as createHttpServer } from 'node:http';
|
|
2
2
|
import { createServer as createHttpsServer } from 'node:https';
|
|
3
3
|
import { Readable } from 'node:stream';
|
|
4
|
-
import { URL } from 'node:url';
|
|
5
4
|
import express from 'express';
|
|
6
5
|
import { BadRequestException, createServerBackedHttpAdapterRealtimeCapability, createErrorResponse, HttpException, InternalServerErrorException, PayloadTooLargeException } from '@fluojs/http';
|
|
6
|
+
import { attachFrameworkRequestNativeRouteHandoff, bindRawRequestNativeRouteHandoff, consumeRawRequestNativeRouteHandoff, isRoutePathNormalizationSensitive } from '@fluojs/http/internal';
|
|
7
7
|
import { createNodeShutdownSignalRegistration, defaultNodeShutdownSignals } from '@fluojs/runtime/node';
|
|
8
|
+
import { cloneRequestHeaders, createDeferredFrameworkRequestShell, createMemoizedAsyncValue, createRequestSignal, normalizePrimaryContentType, parseQueryParamsFromSearch, resolveAbsoluteRequestUrl, resolveRequestIdFromHeaders, snapshotSimpleQueryRecord, splitRawRequestUrl } from '@fluojs/runtime/internal-node';
|
|
8
9
|
import { parseMultipart } from '@fluojs/runtime/web';
|
|
9
10
|
import { bootstrapHttpAdapterApplication, runHttpAdapterApplication } from '@fluojs/runtime/internal/http-adapter';
|
|
10
11
|
import { dispatchWithRequestResponseFactory } from '@fluojs/runtime/internal/request-response-factory';
|
|
@@ -129,6 +130,16 @@ export class ExpressHttpApplicationAdapter {
|
|
|
129
130
|
next();
|
|
130
131
|
return;
|
|
131
132
|
}
|
|
133
|
+
const nativeMethod = request.method.toUpperCase();
|
|
134
|
+
const requestPath = splitRawRequestUrl(request.originalUrl || request.url || '/').path;
|
|
135
|
+
const descriptor = route.descriptorsByMethod[nativeMethod];
|
|
136
|
+
const params = normalizeNativeRouteParams(request.params);
|
|
137
|
+
if (descriptor && !isRoutePathNormalizationSensitive(requestPath) && !hasNativeRouteParamSeparators(params)) {
|
|
138
|
+
bindRawRequestNativeRouteHandoff(request, {
|
|
139
|
+
descriptor,
|
|
140
|
+
params
|
|
141
|
+
});
|
|
142
|
+
}
|
|
132
143
|
void this.handleRequest(request, response);
|
|
133
144
|
});
|
|
134
145
|
}
|
|
@@ -150,38 +161,46 @@ function resolveDispatcherRouteDescriptors(dispatcher) {
|
|
|
150
161
|
function createExpressNativeRoutes(descriptors) {
|
|
151
162
|
const candidates = new Map();
|
|
152
163
|
const shapePaths = new Map();
|
|
164
|
+
const versionSensitiveRouteKeys = collectVersionSensitiveRouteKeys(descriptors);
|
|
153
165
|
for (const descriptor of descriptors) {
|
|
154
|
-
if (descriptor.route.method
|
|
166
|
+
if (!isExpressNativeRouteDescriptor(descriptor) || versionSensitiveRouteKeys.has(`${descriptor.route.method}:${descriptor.route.path}`)) {
|
|
155
167
|
continue;
|
|
156
168
|
}
|
|
157
|
-
registerExpressNativeRouteCandidate(candidates, shapePaths, descriptor
|
|
169
|
+
registerExpressNativeRouteCandidate(candidates, shapePaths, descriptor);
|
|
158
170
|
}
|
|
159
171
|
const routesByPath = new Map();
|
|
160
172
|
for (const candidate of candidates.values()) {
|
|
161
173
|
if (shapePaths.get(candidate.shapeKey)?.size !== 1) {
|
|
162
174
|
continue;
|
|
163
175
|
}
|
|
164
|
-
let
|
|
165
|
-
if (!
|
|
166
|
-
|
|
167
|
-
|
|
176
|
+
let route = routesByPath.get(candidate.path);
|
|
177
|
+
if (!route) {
|
|
178
|
+
route = {
|
|
179
|
+
descriptorsByMethod: {},
|
|
180
|
+
methods: new Set()
|
|
181
|
+
};
|
|
182
|
+
routesByPath.set(candidate.path, route);
|
|
168
183
|
}
|
|
169
|
-
methods.add(candidate.method);
|
|
184
|
+
route.methods.add(candidate.method);
|
|
185
|
+
route.descriptorsByMethod[candidate.method] = candidate.descriptor;
|
|
170
186
|
}
|
|
171
|
-
return [...routesByPath.entries()].map(([path,
|
|
172
|
-
|
|
187
|
+
return [...routesByPath.entries()].map(([path, route]) => ({
|
|
188
|
+
descriptorsByMethod: route.descriptorsByMethod,
|
|
189
|
+
methods: [...route.methods],
|
|
173
190
|
path
|
|
174
191
|
}));
|
|
175
192
|
}
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const nativeMethod = method;
|
|
193
|
+
function isExpressNativeRouteDescriptor(descriptor) {
|
|
194
|
+
return descriptor.route.method !== 'ALL' && EXPRESS_NATIVE_ROUTE_METHODS.includes(descriptor.route.method) && descriptor.route.version === undefined;
|
|
195
|
+
}
|
|
196
|
+
function registerExpressNativeRouteCandidate(candidates, shapePaths, descriptor) {
|
|
197
|
+
const nativeMethod = descriptor.route.method;
|
|
198
|
+
const path = descriptor.route.path;
|
|
181
199
|
const routeKey = `${nativeMethod}:${path}`;
|
|
182
200
|
const shapeKey = `${nativeMethod}:${canonicalizeExpressRouteShape(path)}`;
|
|
183
201
|
if (!candidates.has(routeKey)) {
|
|
184
202
|
candidates.set(routeKey, {
|
|
203
|
+
descriptor,
|
|
185
204
|
method: nativeMethod,
|
|
186
205
|
path,
|
|
187
206
|
shapeKey
|
|
@@ -209,6 +228,9 @@ function createExpressRequestResponseFactory(multipartOptions, maxBodySize = DEF
|
|
|
209
228
|
createResponse(response) {
|
|
210
229
|
return createFrameworkResponse(response);
|
|
211
230
|
},
|
|
231
|
+
async materializeRequest(request) {
|
|
232
|
+
await materializeFrameworkRequestBody(request);
|
|
233
|
+
},
|
|
212
234
|
resolveRequestId(request) {
|
|
213
235
|
return resolveRequestIdFromHeaders(request.headers);
|
|
214
236
|
},
|
|
@@ -281,6 +303,18 @@ function createFrameworkResponse(response) {
|
|
|
281
303
|
this.committed = true;
|
|
282
304
|
response.send(serialized.payload);
|
|
283
305
|
},
|
|
306
|
+
async sendSimpleJson(body) {
|
|
307
|
+
if (response.writableEnded) {
|
|
308
|
+
this.committed = true;
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const serialized = serializeResponseBody(body);
|
|
312
|
+
if (!response.hasHeader('content-type') && serialized.defaultContentType) {
|
|
313
|
+
response.setHeader('content-type', serialized.defaultContentType);
|
|
314
|
+
}
|
|
315
|
+
this.committed = true;
|
|
316
|
+
response.send(serialized.payload);
|
|
317
|
+
},
|
|
284
318
|
setHeader(name, value) {
|
|
285
319
|
const lowerName = name.toLowerCase();
|
|
286
320
|
if (lowerName === 'set-cookie') {
|
|
@@ -321,11 +355,19 @@ function createFrameworkResponseStream(response) {
|
|
|
321
355
|
};
|
|
322
356
|
},
|
|
323
357
|
waitForDrain() {
|
|
324
|
-
if (response.writableEnded) {
|
|
358
|
+
if (response.writableEnded || response.destroyed) {
|
|
325
359
|
return Promise.resolve();
|
|
326
360
|
}
|
|
327
361
|
return new Promise(resolve => {
|
|
328
|
-
|
|
362
|
+
const settle = () => {
|
|
363
|
+
response.removeListener('drain', settle);
|
|
364
|
+
response.removeListener('close', settle);
|
|
365
|
+
response.removeListener('error', settle);
|
|
366
|
+
resolve();
|
|
367
|
+
};
|
|
368
|
+
response.once('drain', settle);
|
|
369
|
+
response.once('close', settle);
|
|
370
|
+
response.once('error', settle);
|
|
329
371
|
});
|
|
330
372
|
},
|
|
331
373
|
write(chunk) {
|
|
@@ -335,44 +377,68 @@ function createFrameworkResponseStream(response) {
|
|
|
335
377
|
}
|
|
336
378
|
async function createFrameworkRequest(request, signal, multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false) {
|
|
337
379
|
const rawUrl = request.originalUrl || request.url || '/';
|
|
338
|
-
const
|
|
339
|
-
const headers = normalizeHeaders(request.headers);
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
let
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
380
|
+
const urlParts = splitRawRequestUrl(rawUrl);
|
|
381
|
+
const headers = normalizeHeaders(cloneRequestHeaders(request.headers));
|
|
382
|
+
const querySnapshot = snapshotSimpleQueryRecord(request.query);
|
|
383
|
+
const contentType = normalizePrimaryContentType(headers['content-type']);
|
|
384
|
+
const isMultipart = contentType === 'multipart/form-data';
|
|
385
|
+
let frameworkRequest;
|
|
386
|
+
const materializeBody = createMemoizedAsyncValue(async () => {
|
|
387
|
+
if (isMultipart) {
|
|
388
|
+
const parsed = await parseMultipartRequest(request, {
|
|
389
|
+
...multipartOptions,
|
|
390
|
+
maxTotalSize: multipartOptions?.maxTotalSize ?? maxBodySize
|
|
391
|
+
});
|
|
392
|
+
frameworkRequest.body = parsed.fields;
|
|
393
|
+
frameworkRequest.files = parsed.files;
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
353
396
|
const bodyResult = await readRequestBody(request, headers['content-type'], maxBodySize, preserveRawBody);
|
|
354
|
-
body = bodyResult.body;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
397
|
+
frameworkRequest.body = bodyResult.body;
|
|
398
|
+
if (bodyResult.rawBody) {
|
|
399
|
+
frameworkRequest.rawBody = bodyResult.rawBody;
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
frameworkRequest = createDeferredFrameworkRequestShell({
|
|
403
|
+
cookieHeader: headers.cookie,
|
|
360
404
|
headers,
|
|
405
|
+
materializeBody,
|
|
361
406
|
method: request.method,
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
407
|
+
path: urlParts.path,
|
|
408
|
+
query: querySnapshot,
|
|
409
|
+
queryFactory: () => parseQueryParamsFromSearch(urlParts.search),
|
|
365
410
|
raw: request,
|
|
366
411
|
signal,
|
|
367
|
-
url:
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
|
|
412
|
+
url: urlParts.path + urlParts.search
|
|
413
|
+
});
|
|
414
|
+
const nativeRouteHandoff = consumeRawRequestNativeRouteHandoff(request);
|
|
415
|
+
return nativeRouteHandoff ? attachFrameworkRequestNativeRouteHandoff(frameworkRequest, nativeRouteHandoff) : frameworkRequest;
|
|
416
|
+
}
|
|
417
|
+
function normalizeNativeRouteParams(params) {
|
|
418
|
+
if (typeof params !== 'object' || params === null) {
|
|
419
|
+
return {};
|
|
371
420
|
}
|
|
372
|
-
|
|
373
|
-
|
|
421
|
+
return Object.fromEntries(Object.entries(params).flatMap(([key, value]) => typeof value === 'string' ? [[key, value]] : value === undefined ? [] : [[key, String(value)]]));
|
|
422
|
+
}
|
|
423
|
+
function hasNativeRouteParamSeparators(params) {
|
|
424
|
+
return Object.values(params).some(value => value.includes('/'));
|
|
425
|
+
}
|
|
426
|
+
function collectVersionSensitiveRouteKeys(descriptors) {
|
|
427
|
+
const grouped = new Map();
|
|
428
|
+
for (const descriptor of descriptors) {
|
|
429
|
+
if (!EXPRESS_NATIVE_ROUTE_METHODS.includes(descriptor.route.method)) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const routeKey = `${descriptor.route.method}:${descriptor.route.path}`;
|
|
433
|
+
const current = grouped.get(routeKey) ?? {
|
|
434
|
+
count: 0,
|
|
435
|
+
hasVersioned: false
|
|
436
|
+
};
|
|
437
|
+
current.count += 1;
|
|
438
|
+
current.hasVersioned ||= descriptor.route.version !== undefined;
|
|
439
|
+
grouped.set(routeKey, current);
|
|
374
440
|
}
|
|
375
|
-
return
|
|
441
|
+
return new Set([...grouped.entries()].filter(([, current]) => current.count > 1 || current.hasVersioned).map(([routeKey]) => routeKey));
|
|
376
442
|
}
|
|
377
443
|
async function parseMultipartRequest(request, options = {}) {
|
|
378
444
|
try {
|
|
@@ -380,7 +446,7 @@ async function parseMultipartRequest(request, options = {}) {
|
|
|
380
446
|
body: Readable.toWeb(request),
|
|
381
447
|
headers: normalizeHeaders(request.headers),
|
|
382
448
|
method: request.method,
|
|
383
|
-
url:
|
|
449
|
+
url: resolveAbsoluteRequestUrl(request.url)
|
|
384
450
|
}, options);
|
|
385
451
|
} catch (error) {
|
|
386
452
|
if (isExpressMultipartTooLargeError(error)) {
|
|
@@ -437,57 +503,6 @@ function normalizeHeaders(headers) {
|
|
|
437
503
|
}
|
|
438
504
|
return normalized;
|
|
439
505
|
}
|
|
440
|
-
function parseQueryParams(searchParams) {
|
|
441
|
-
const query = {};
|
|
442
|
-
for (const [key, value] of searchParams.entries()) {
|
|
443
|
-
const current = query[key];
|
|
444
|
-
if (current === undefined) {
|
|
445
|
-
query[key] = value;
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
if (Array.isArray(current)) {
|
|
449
|
-
current.push(value);
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
query[key] = [current, value];
|
|
453
|
-
}
|
|
454
|
-
return query;
|
|
455
|
-
}
|
|
456
|
-
function parseCookieHeader(cookieHeader) {
|
|
457
|
-
if (!cookieHeader) {
|
|
458
|
-
return {};
|
|
459
|
-
}
|
|
460
|
-
return Object.fromEntries(cookieHeader.split(';').map(pair => pair.trim()).filter(Boolean).map(pair => {
|
|
461
|
-
const index = pair.indexOf('=');
|
|
462
|
-
if (index === -1) {
|
|
463
|
-
return [pair.trim(), ''];
|
|
464
|
-
}
|
|
465
|
-
const rawValue = pair.slice(index + 1).trim();
|
|
466
|
-
try {
|
|
467
|
-
return [pair.slice(0, index).trim(), decodeURIComponent(rawValue)];
|
|
468
|
-
} catch {
|
|
469
|
-
return [pair.slice(0, index).trim(), rawValue];
|
|
470
|
-
}
|
|
471
|
-
}));
|
|
472
|
-
}
|
|
473
|
-
function createRequestSignal(response) {
|
|
474
|
-
const controller = new AbortController();
|
|
475
|
-
const abort = reason => {
|
|
476
|
-
if (!controller.signal.aborted) {
|
|
477
|
-
controller.abort(new Error(reason));
|
|
478
|
-
}
|
|
479
|
-
};
|
|
480
|
-
response.once('close', () => {
|
|
481
|
-
if (!response.writableEnded) {
|
|
482
|
-
abort('Response closed before response commit.');
|
|
483
|
-
}
|
|
484
|
-
});
|
|
485
|
-
return controller.signal;
|
|
486
|
-
}
|
|
487
|
-
function resolveRequestIdFromHeaders(headers) {
|
|
488
|
-
const requestId = headers['x-request-id'] ?? headers['x-correlation-id'];
|
|
489
|
-
return Array.isArray(requestId) ? requestId[0] : requestId;
|
|
490
|
-
}
|
|
491
506
|
function createExpressServer(httpsOptions, app) {
|
|
492
507
|
return httpsOptions ? createHttpsServer(httpsOptions, app) : createHttpServer(app);
|
|
493
508
|
}
|
|
@@ -658,8 +673,8 @@ async function readRequestBody(request, contentType, maxBodySize = DEFAULT_MAX_B
|
|
|
658
673
|
rawBody: preserveRawBody ? rawBody : undefined
|
|
659
674
|
};
|
|
660
675
|
}
|
|
661
|
-
const primaryContentType =
|
|
662
|
-
if (
|
|
676
|
+
const primaryContentType = normalizePrimaryContentType(contentType);
|
|
677
|
+
if (primaryContentType === 'application/json') {
|
|
663
678
|
try {
|
|
664
679
|
return {
|
|
665
680
|
body: JSON.parse(bodyText),
|
|
@@ -669,7 +684,7 @@ async function readRequestBody(request, contentType, maxBodySize = DEFAULT_MAX_B
|
|
|
669
684
|
throw new BadRequestException('Request body contains invalid JSON.');
|
|
670
685
|
}
|
|
671
686
|
}
|
|
672
|
-
if (
|
|
687
|
+
if (primaryContentType === 'application/x-www-form-urlencoded') {
|
|
673
688
|
return {
|
|
674
689
|
body: parseUrlEncodedBody(bodyText),
|
|
675
690
|
rawBody: preserveRawBody ? rawBody : undefined
|
|
@@ -688,11 +703,9 @@ function parseUrlEncodedBody(bodyText) {
|
|
|
688
703
|
}
|
|
689
704
|
return fields;
|
|
690
705
|
}
|
|
691
|
-
function
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
}
|
|
695
|
-
return headerValue;
|
|
706
|
+
async function materializeFrameworkRequestBody(request) {
|
|
707
|
+
await request.materializeBody?.();
|
|
708
|
+
delete request.materializeBody;
|
|
696
709
|
}
|
|
697
710
|
function setMultiValue(target, key, value) {
|
|
698
711
|
const existing = target[key];
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"platform",
|
|
9
9
|
"server"
|
|
10
10
|
],
|
|
11
|
-
"version": "1.0.0-beta.
|
|
11
|
+
"version": "1.0.0-beta.5",
|
|
12
12
|
"private": false,
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"repository": {
|
|
@@ -36,12 +36,13 @@
|
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"express": "^5.1.0",
|
|
39
|
-
"@fluojs/http": "^1.0.0-beta.
|
|
40
|
-
"@fluojs/runtime": "^1.0.0-beta.
|
|
39
|
+
"@fluojs/http": "^1.0.0-beta.5",
|
|
40
|
+
"@fluojs/runtime": "^1.0.0-beta.7"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/express": "^5.0.3",
|
|
44
|
-
"vitest": "^3.2.4"
|
|
44
|
+
"vitest": "^3.2.4",
|
|
45
|
+
"@fluojs/di": "^1.0.0-beta.5"
|
|
45
46
|
},
|
|
46
47
|
"scripts": {
|
|
47
48
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|