@fluojs/platform-fastify 1.0.8 → 2.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 +98 -9
- package/README.md +98 -9
- package/dist/adapter.d.ts +25 -13
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +284 -121
- package/package.json +9 -8
package/README.ko.md
CHANGED
|
@@ -7,6 +7,7 @@ fluo 런타임을 위한 Fastify 기반 HTTP 어댑터 패키지입니다.
|
|
|
7
7
|
## 목차
|
|
8
8
|
|
|
9
9
|
- [설치](#설치)
|
|
10
|
+
- [런타임 요구 사항](#런타임-요구-사항)
|
|
10
11
|
- [사용 시점](#사용-시점)
|
|
11
12
|
- [빠른 시작](#빠른-시작)
|
|
12
13
|
- [주요 패턴](#주요-패턴)
|
|
@@ -20,9 +21,17 @@ fluo 런타임을 위한 Fastify 기반 HTTP 어댑터 패키지입니다.
|
|
|
20
21
|
## 설치
|
|
21
22
|
|
|
22
23
|
```bash
|
|
23
|
-
npm install @fluojs/platform-fastify
|
|
24
|
+
npm install @fluojs/platform-fastify
|
|
24
25
|
```
|
|
25
26
|
|
|
27
|
+
`fastify`, `@fastify/multipart`, raw-body 지원은 이 adapter package의 runtime dependency로 포함되어 있으므로, 애플리케이션이 fluo 밖에서 Fastify API를 직접 사용하지 않는 한 별도의 `fastify` dependency를 추가할 필요가 없습니다.
|
|
28
|
+
|
|
29
|
+
## 런타임 요구 사항
|
|
30
|
+
|
|
31
|
+
`@fluojs/platform-fastify`는 Node.js HTTP adapter이며 `engines.node >=24.0.0 <27`을 선언합니다. Listener-level RFC `QUERY` 요청이 Fastify wildcard fallback과 fluo dispatch에 도달하도록 이 패키지가 HTTP 서버를 소유하는 로컬 개발, CI, 컨테이너, 프로덕션 호스트는 이 정확한 범위의 Node.js에서 실행해야 합니다. Node 24 미만과 Node 27 이상은 제외됩니다. 비 Node 런타임에서는 이 Node 전용 adapter를 import하지 말고 `@fluojs/platform-bun`, `@fluojs/platform-deno`, 또는 `@fluojs/platform-cloudflare-workers`를 사용하세요.
|
|
32
|
+
|
|
33
|
+
어댑터는 Fastify 기반 Node `http` 또는 `https` listener를 소유합니다. 포트, 인증서 material, hostname 같은 process-specific value는 애플리케이션 경계에 두고, 최종 option만 adapter에 명시적으로 전달하세요.
|
|
34
|
+
|
|
26
35
|
## 사용 시점
|
|
27
36
|
|
|
28
37
|
fluo 애플리케이션을 위한 고성능 HTTP 어댑터가 필요한 경우 이 패키지를 사용합니다. Fastify는 낮은 오버헤드와 효율적인 요청 처리로 잘 알려져 있으며, 높은 처리량과 동시성이 요구되는 프로덕션 fluo 애플리케이션에 권장되는 선택입니다.
|
|
@@ -41,12 +50,53 @@ const app = await fluoFactory.create(AppModule, {
|
|
|
41
50
|
await app.listen();
|
|
42
51
|
```
|
|
43
52
|
|
|
44
|
-
`createFastifyAdapter()`는 기본 port로 `3000`을 사용하며 `process.env.PORT`를 읽지 않습니다. `port`, `maxBodySize`, `retryDelayMs`, `retryLimit`, `shutdownTimeoutMs` 같은 잘못된 explicit numeric option은 adapter setup 중 throw됩니다. `maxBodySize`와 `shutdownTimeoutMs`는 음수가 아닌 정수 byte/time limit이므로 `0`도 유효합니다. `maxBodySize: 0`은 빈 request body만 허용하고, `shutdownTimeoutMs: 0`은 다음 timer turn에 Fastify close
|
|
53
|
+
`createFastifyAdapter()`는 기본 port로 `3000`을 사용하며 `process.env.PORT`를 읽지 않습니다. `port`, `maxBodySize`, `retryDelayMs`, `retryLimit`, `shutdownTimeoutMs` 같은 잘못된 explicit numeric option은 adapter setup 중 throw됩니다. `maxBodySize`와 `shutdownTimeoutMs`는 음수가 아닌 정수 byte/time limit이므로 `0`도 유효합니다. `maxBodySize: 0`은 빈 request body만 허용하고, `shutdownTimeoutMs: 0`은 Fastify close를 즉시 시작합니다. `0`은 대기 시간만 제한하므로 close가 아직 settle되지 않았다면 대기는 다음 timer turn에 timeout될 수 있지만, 기반 Fastify close와 cleanup은 계속 진행됩니다.
|
|
45
54
|
|
|
46
55
|
## 주요 패턴
|
|
47
56
|
|
|
57
|
+
### Early Hints
|
|
58
|
+
|
|
59
|
+
Fastify response는 `reply.raw`를 통해 optional `context.response.earlyHints` capability를 노출합니다. `103` 하나마다 `write(...)`를 await하면 독립적으로 설정된 final response보다 먼저 여러 informational response가 전송됩니다. Early field는 Fastify final header를 채우거나 Fluo facade를 committed 상태로 만들지 않습니다. Late/native failure와 client disconnect는 결정적으로 reject됩니다.
|
|
60
|
+
|
|
61
|
+
### 바이트 범위와 캐시 검증
|
|
62
|
+
|
|
63
|
+
Fastify는 공유 `@fluojs/http` 단일 byte-range 및 `If-Range` contract를 보존합니다. Conditional-request 평가가 cache validator를 선택한 뒤 유효한 `Range: bytes=` 요청은 portable `206` identity-byte response를 만들고, `If-Range`는 선택된 validator를 재사용합니다. Malformed 또는 multi-range field는 전체 response를 유지하고 충족 불가능한 range는 body 없는 `416`을 만들며, `HEAD`는 stream을 소비하지 않고 GET metadata를 반영합니다.
|
|
64
|
+
|
|
65
|
+
### HTTPS/TLS 시작
|
|
66
|
+
Fastify 프로세스가 TLS를 직접 소유할 때는 Node.js `https.ServerOptions`를 `createFastifyAdapter(...)`, `bootstrapFastifyApplication(...)`, 또는 `runFastifyApplication(...)`의 `https` option으로 전달하세요. Adapter는 Fastify를 HTTPS listener로 시작하며 startup log는 `https://host:port` URL을 보고합니다.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
const app = await fluoFactory.create(AppModule, {
|
|
70
|
+
adapter: createFastifyAdapter({
|
|
71
|
+
host: '0.0.0.0',
|
|
72
|
+
port: 3443,
|
|
73
|
+
https: {
|
|
74
|
+
cert: tlsCertificate,
|
|
75
|
+
key: tlsPrivateKey,
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await app.listen();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Adapter를 만들기 전에 certificate는 애플리케이션 configuration 또는 secret-management boundary에서 로드하세요. 이 패키지는 certificate file, `process.env`, `PORT`를 직접 읽지 않습니다. Load balancer, ingress, API gateway가 TLS를 종료한다면 `https`를 설정하지 말고 해당 infrastructure 뒤에서 Fastify adapter를 일반 HTTP로 실행하세요.
|
|
84
|
+
|
|
85
|
+
`bootstrapFastifyApplication(...)`과 `runFastifyApplication(...)`도 같은 `https`, `host`, `port` option을 받습니다. `runFastifyApplication(...)`은 resolve되기 전에 listening을 시작하고 shutdown registration을 설치한 다음 실행 중인 application shell을 반환합니다.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
const app = await runFastifyApplication(AppModule, {
|
|
89
|
+
host: '127.0.0.1',
|
|
90
|
+
https: {
|
|
91
|
+
cert: tlsCertificate,
|
|
92
|
+
key: tlsPrivateKey,
|
|
93
|
+
},
|
|
94
|
+
port: 3443,
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
48
98
|
### 멀티파트 및 Raw Body
|
|
49
|
-
Fastify 어댑터는 내부 Fastify 플러그인을 통해 멀티파트 form-data 및 raw body 파싱을 기본적으로 지원하며, 이는 표준 fluo 요청 인터페이스를 통해 노출됩니다. `rawBody: true`를 활성화하면 멀티파트가 아닌 요청에서 `FrameworkRequest.rawBody`가 원본 요청 바이트를 그대로 보존하므로 webhook 서명 검증이나 기타 바이트 민감한 흐름에서 정확한 payload를 다시 사용할 수 있습니다. 어댑터를 직접 생성할 때는 멀티파트 제한을 두 번째 인자로 전달하고, `bootstrapFastifyApplication(...)` 및 `runFastifyApplication(...)`에서는 같은 설정을 `options.multipart` 아래에 전달하면 됩니다.
|
|
99
|
+
Fastify 어댑터는 내부 Fastify 플러그인을 통해 멀티파트 form-data 및 raw body 파싱을 기본적으로 지원하며, 이는 표준 fluo 요청 인터페이스를 통해 노출됩니다. Multipart file은 runtime-neutral `FrameworkRequest.files` seam에 adapter-provided value로 붙으며, Fastify 요청에서는 body materialization 이후 fluo `UploadedFile` 객체로 채워집니다. `rawBody: true`를 활성화하면 멀티파트가 아닌 요청에서 `FrameworkRequest.rawBody`가 원본 요청 바이트를 그대로 보존하므로 webhook 서명 검증이나 기타 바이트 민감한 흐름에서 정확한 payload를 다시 사용할 수 있습니다. 어댑터를 직접 생성할 때는 멀티파트 제한을 두 번째 인자로 전달하고, `bootstrapFastifyApplication(...)` 및 `runFastifyApplication(...)`에서는 같은 설정을 `options.multipart` 아래에 전달하면 됩니다.
|
|
50
100
|
|
|
51
101
|
Multipart request에서는 `Multipart/Form-Data`처럼 대소문자가 섞인 `Content-Type` media 값도 포함해 raw-body capture를 건너뜁니다. `multipart.maxTotalSize`를 생략하면 `maxBodySize`가 기본값이 되어 HTTP adapter 간 size limit이 portable하게 유지됩니다.
|
|
52
102
|
|
|
@@ -62,6 +112,14 @@ const adapter = createFastifyAdapter(
|
|
|
62
112
|
);
|
|
63
113
|
```
|
|
64
114
|
|
|
115
|
+
### 네이티브 Raw Request 및 Response 객체
|
|
116
|
+
|
|
117
|
+
기본적으로 `RequestContext`의 이식 가능한 `FrameworkRequest` 및 `FrameworkResponse` field와 method를 사용하세요. `context.request`로 request 데이터를 읽고 `context.response`로 status, header, redirect, body를 작성하면 controller가 fluo HTTP adapter 간에 이식성을 유지합니다.
|
|
118
|
+
|
|
119
|
+
Fastify의 공유 `raw` field는 의도적으로 비대칭입니다. `context.request.raw`는 기반 Node.js `IncomingMessage`이고 `context.response.raw`는 Fastify의 `FastifyReply`입니다. Request raw 객체는 `FastifyRequest`가 아니며 response raw 객체는 Node.js `ServerResponse`가 아닙니다.
|
|
120
|
+
|
|
121
|
+
NestJS `@Req()` 또는 `@Res()` 코드를 마이그레이션할 때 이 `unknown` field에 안전하지 않은 cast를 의존하지 마세요. 이 adapter는 현재 typed Fastify-native request accessor를 공개하지 않습니다. 이식 가능한 framework 작업으로 충족할 수 없는 native Fastify 요구 사항이 있으면 shared controller 코드를 cast에 결합하지 말고 계약을 보존하는 typed accessor를 요청하세요.
|
|
122
|
+
|
|
65
123
|
### 서버 기반 실시간 통신 (Real-Time)
|
|
66
124
|
Fastify는 `@fluojs/websockets`가 기본 Node.js HTTP 서버에 직접 연결될 수 있도록 `server-backed` 기능을 제공합니다.
|
|
67
125
|
|
|
@@ -123,12 +181,35 @@ await bootstrapFastifyApplication(AppModule, {
|
|
|
123
181
|
});
|
|
124
182
|
```
|
|
125
183
|
|
|
184
|
+
### 네이티브 Fastify 설정
|
|
185
|
+
기본적으로는 이식 가능한 fluo 미들웨어를 사용하세요. 마이그레이션에서 Fastify 전용 플러그인, hook 또는 인스턴스 customisation을 유지해야 할 때는 construction-time `configureFastify` seam으로 설정합니다.
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const adapter = createFastifyAdapter({
|
|
189
|
+
configureFastify: async (fastify) => {
|
|
190
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
191
|
+
reply.header('x-native-request-id', request.id);
|
|
192
|
+
});
|
|
193
|
+
fastify.setReplySerializer((payload) => JSON.stringify(payload) ?? '');
|
|
194
|
+
},
|
|
195
|
+
port: 3000,
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`configureFastify`는 어댑터가 생성하는 각 Fastify 인스턴스마다 한 번 실행되며, fluo가 multipart, raw-body, native-route, wildcard-route 처리를 등록하기 전에 완료됩니다. `bootstrapFastifyApplication(...)`과 `runFastifyApplication(...)`도 같은 옵션을 받습니다. 설정이 throw 또는 reject되면 해당 `listen()` 호출은 시작되지 않습니다. 실패한 인스턴스에는 설정을 다시 적용하지 않으며, 성공적으로 `close()`한 뒤의 다음 `listen()`은 새 인스턴스를 한 번 설정합니다. 이 seam에서 `setReplySerializer(...)`를 호출할 수는 있지만, 어댑터는 Fastify에 넘기기 전에 fluo response payload를 직렬화하므로 instance serializer는 fluo response를 customisation하지 않습니다.
|
|
200
|
+
|
|
201
|
+
어댑터는 routing, CORS, logging, multipart와 raw-body 동작, response semantics, shutdown의 소유권을 계속 가집니다. Bootstrap 뒤 Fastify 인스턴스를 보관하거나 변경하지 말고, 기존 Fastify 인스턴스를 adoption하거나 이 hook을 native-route bypass로 사용하지 마세요. 이식 가능한 request 동작은 fluo `middleware`로 옮기세요.
|
|
202
|
+
|
|
126
203
|
### 네이티브 라우트 등록과 안전한 폴백
|
|
127
204
|
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 의미론은 그대로 유지됩니다.
|
|
128
205
|
|
|
129
206
|
여러 라우트가 같은 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된 요청을 다시 매칭합니다.
|
|
130
207
|
|
|
131
|
-
|
|
208
|
+
`QUERY`, `PURGE` 같은 검증된 custom method도 native fluo route handoff를 받지 않고 wildcard fallback dispatch에 남습니다. Fastify wildcard route가 요청을 받으려면 method 이름을 미리 알아야 하므로 adapter는 descriptor의 custom method를 body-bearing Fastify method로 등록하되 route selection은 계속 fluo에 맡깁니다. `ALL`은 wire method로 등록하지 않으며 `CONNECT`는 일반 controller routing conformance 범위 밖에 유지됩니다.
|
|
209
|
+
|
|
210
|
+
어댑터는 매칭되지 않은 경로와 이식성에 민감한 경우, 그리고 공유 body/materialization 경로를 보존해야 하는 multipart request를 위해 와일드카드 fallback 라우트를 계속 유지하며, Fastify의 trailing slash / duplicate slash 정규화를 켜서 네이티브 선택 경로도 fluo의 문서화된 route path 계약과 맞추어 동작하도록 합니다. CORS 처리는 Fastify 플러그인이 아니라 fluo의 공유 middleware 경로가 계속 소유하고, `OPTIONS` 같은 미지원 메서드는 fluo route가 명시적으로 소유하지 않는 한 fallback dispatcher 경로로 흐릅니다.
|
|
211
|
+
|
|
212
|
+
동시에 호출된 `listen()`은 하나의 startup promise를 공유하고 첫 번째 호출의 dispatcher를 유지합니다. Startup 이후 반복되는 `listen()` 호출은 live listener와 dispatcher를 변경하지 않는 no-op입니다. `close()`가 진행 중일 때 호출된 `listen()`은 shutdown이 settle될 때까지 기다린 뒤 새 listener를 시작하며, 해당 listener가 준비된 후에만 resolve됩니다. 바쁜 port 때문에 startup retry 중인 상태에서 `close()`를 호출하면 retry loop를 취소하고 해당 작업이 settle될 때까지 기다린 뒤 shutdown 완료를 보고하므로, caller가 shutdown이 끝났다고 믿은 뒤 닫힌 adapter가 나중에 bind되는 일이 없습니다. Adapter instance를 close 이후 다시 listen하면 native route handler가 traffic을 처리하기 전에 dispatcher descriptor를 새로 반영하므로 request handoff metadata가 이전 application graph를 가리키지 않습니다.
|
|
132
213
|
|
|
133
214
|
## 성능
|
|
134
215
|
|
|
@@ -143,15 +224,15 @@ fluo의 Fastify 어댑터는 높은 동시성 시나리오에서 raw Node.js 어
|
|
|
143
224
|
|
|
144
225
|
## 적합성 커버리지
|
|
145
226
|
|
|
146
|
-
`packages/platform-fastify/src/adapter.test.ts`는 문서화된 Fastify 어댑터 계약을 위한 package-local regression target입니다. 이 파일은 공유 `createHttpAdapterPortabilityHarness(...)` 검사를 실행하여 malformed cookie 보존, JSON/text raw-body capture, byte-exact raw-body capture, multipart raw-body 제외, multipart total-size 기본값, SSE framing, response stream drain settlement, host 및 HTTPS startup logging, shutdown signal listener cleanup을 확인합니다.
|
|
227
|
+
`packages/platform-fastify/src/adapter.test.ts`는 문서화된 Fastify 어댑터 계약을 위한 package-local regression target입니다. 이 파일은 공유 `createHttpAdapterPortabilityHarness(...)` 검사를 실행하여 conditional request, single-byte range 및 `If-Range`, custom `QUERY`/extension-method fallback, malformed cookie 보존, JSON/text raw-body capture, byte-exact raw-body capture, multipart raw-body 제외, multipart total-size 기본값, SSE framing, response stream drain settlement, host 및 HTTPS startup logging, shutdown signal listener cleanup을 확인합니다.
|
|
147
228
|
|
|
148
|
-
같은 파일은 Fastify 전용 native route registration과 wildcard fallback, duplicate shape route fallback, middleware/guard/interceptor/observer ordering, CORS ownership, global prefix behavior, malformed cookie preservation, response serialization parity, raw-body pre-parsing behavior, 대소문자 구분 없는 multipart detection, multipart limit handling도 함께 다룹니다. startup, routing, adapter portability behavior를 변경할 때는 README 예제 포인터를 이 테스트 파일 및 custom adapter book chapter와 맞추어 유지하세요.
|
|
229
|
+
같은 파일은 Fastify 전용 native route registration과 wildcard fallback, duplicate shape route fallback, concurrent/repeated `listen()` idempotency, shutdown 중 startup retry cancellation, adapter reuse 시 native descriptor refresh, explicit `OPTIONS` route ownership, middleware/guard/interceptor/observer ordering, CORS ownership, global prefix behavior, malformed cookie preservation, response serialization parity, raw-body pre-parsing behavior, zero-valued body/shutdown limit, 기반 Fastify close를 계속 in-flight 상태로 두는 close 대기 timeout, 대소문자 구분 없는 multipart detection, multipart limit handling도 함께 다룹니다. startup, routing, adapter portability behavior를 변경할 때는 README 예제 포인터를 이 테스트 파일 및 custom adapter book chapter와 맞추어 유지하세요.
|
|
149
230
|
|
|
150
231
|
## 공개 API 개요
|
|
151
232
|
|
|
152
|
-
- `createFastifyAdapter(options, multipartOptions?)`: Fastify 어댑터를 위한 권장 팩토리입니다. 선택적 두 번째 인자는 직접 어댑터를 생성할 때 `maxFileSize`, `maxFiles`, `maxTotalSize` 같은 multipart 제한을 설정합니다.
|
|
153
|
-
- `bootstrapFastifyApplication(module, options)`: 암시적 리스닝 없이 수행하는 고급 부트스트랩입니다.
|
|
154
|
-
- `runFastifyApplication(module, options)`:
|
|
233
|
+
- `createFastifyAdapter(options, multipartOptions?)`: Fastify 어댑터를 위한 권장 팩토리입니다. `options`에는 `host`, `port`, Node.js `https` server option 같은 transport startup knob이 포함됩니다. 선택적 두 번째 인자는 직접 어댑터를 생성할 때 `maxFileSize`, `maxFiles`, `maxTotalSize` 같은 multipart 제한을 설정합니다.
|
|
234
|
+
- `bootstrapFastifyApplication(module, options)`: 암시적 리스닝 없이 수행하는 고급 부트스트랩입니다. Host가 bind 전에 앱을 구성해야 할 때 `https`를 포함한 같은 Fastify startup option을 받습니다.
|
|
235
|
+
- `runFastifyApplication(module, options)`: Application을 bootstrap하고 listening을 시작한 뒤 shutdown registration을 설치하며, 같은 `https` startup surface를 사용하는 실행 중인 shell을 반환합니다. Signal 기반 shutdown timeout/실패 시에는 해당 상태를 로그와 `process.exitCode`로 보고하고, 최종 프로세스 종료는 주변 호스트에 맡깁니다.
|
|
155
236
|
- `isFastifyMultipartTooLargeError(error)`: Fastify error shape 전반에서 multipart limit error를 감지합니다.
|
|
156
237
|
- `FastifyHttpApplicationAdapter`: 핵심 어댑터 구현 클래스입니다.
|
|
157
238
|
- Option type: `FastifyAdapterOptions`, `BootstrapFastifyApplicationOptions`, `RunFastifyApplicationOptions`, `CorsInput`, `FastifyApplicationSignal`.
|
|
@@ -163,6 +244,14 @@ fluo의 Fastify 어댑터는 높은 동시성 시나리오에서 raw Node.js 어
|
|
|
163
244
|
- **로깅 (Logging)**: 로그 스트림 중복을 방지하기 위해 Fastify의 네이티브 로거가 비활성화됩니다. `runFastifyApplication`과 `bootstrapFastifyApplication`은 framework console logger를 기본으로 선택하며, host나 test가 주입된 `ApplicationLogger`를 사용해야 할 때 `logger`를 받습니다.
|
|
164
245
|
- **글로벌 접두사 (Global Prefix)**: 내부 경로 또는 헬스 체크 엔드포인트에 접두사가 붙지 않도록 `globalPrefixExclude`를 적절히 설정하세요.
|
|
165
246
|
- **Malformed Cookie**: 잘못된 cookie header는 request 실패로 이어지지 않고 보존됩니다.
|
|
247
|
+
- **HTTPS 시작**: Fastify 프로세스가 TLS를 소유한다면 Node.js `>=24.0.0 <27`에서 adapter `https` option 아래에 certificate material을 전달하세요. Infrastructure가 TLS를 종료한다면 해당 경계 뒤에서 adapter를 일반 HTTP로 유지하세요.
|
|
248
|
+
- **시작 및 종료 실패**: startup과 Fastify `onClose`가 모두 실패하면, `cause`를 읽고 쓰고 다시 읽을 수 있는 경우에만 caller는 원래 startup rejection과 `cause`의 close failure를 받습니다. 그 외에는 caller가 startup rejection을 `errors[0]`, close failure를 `errors[1]`로 갖는 startup-first `AggregateError`를 받습니다.
|
|
249
|
+
|
|
250
|
+
## Multipart 스트리밍
|
|
251
|
+
|
|
252
|
+
Fastify를 bootstrap할 때 `multipart: { strategy: 'stream' }`을 설정하면 multipart part가 `RequestContext.request.body`의 `AsyncIterable`로 노출됩니다. Fastify는 iterator를 미리 읽거나 버퍼링하지 않으며, file part를 소비할 때만 바이트를 가져옵니다. 이 mode에서는 file part가 `request.files`의 `UploadedFile` 값으로 materialize되지 않습니다. 버퍼링 multipart parsing은 기본값이며 하나의 request body에서 stream 소비와 함께 사용할 수 없습니다.
|
|
253
|
+
|
|
254
|
+
Runtime route dispatch는 route를 위해 만든 iterator를 소유하며 handler가 끝난 뒤 자동으로 `return()`을 호출해 active source를 cancel하고 release합니다. Standalone `parseMultipartStream(...)` consumer는 이 책임을 직접 집니다. iterator를 끝까지 소비하거나 일찍 끝낼 때 `return()`을 호출하세요.
|
|
166
255
|
|
|
167
256
|
## 관련 패키지
|
|
168
257
|
|
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Fastify-backed HTTP adapter for the fluo runtime.
|
|
|
7
7
|
## Table of Contents
|
|
8
8
|
|
|
9
9
|
- [Installation](#installation)
|
|
10
|
+
- [Runtime Requirements](#runtime-requirements)
|
|
10
11
|
- [When to Use](#when-to-use)
|
|
11
12
|
- [Quick Start](#quick-start)
|
|
12
13
|
- [Common Patterns](#common-patterns)
|
|
@@ -20,9 +21,17 @@ Fastify-backed HTTP adapter for the fluo runtime.
|
|
|
20
21
|
## Installation
|
|
21
22
|
|
|
22
23
|
```bash
|
|
23
|
-
npm install @fluojs/platform-fastify
|
|
24
|
+
npm install @fluojs/platform-fastify
|
|
24
25
|
```
|
|
25
26
|
|
|
27
|
+
`fastify`, `@fastify/multipart`, and raw-body support are bundled as runtime dependencies of this adapter package, so application projects do not need a separate `fastify` dependency unless they use Fastify APIs directly outside fluo.
|
|
28
|
+
|
|
29
|
+
## Runtime Requirements
|
|
30
|
+
|
|
31
|
+
`@fluojs/platform-fastify` is a Node.js HTTP adapter and declares `engines.node >=24.0.0 <27`. Run local development, CI, containers, and production hosts on a version in that exact range when this package owns the HTTP server so listener-level RFC `QUERY` requests reach Fastify wildcard fallback and fluo dispatch. Node versions below 24 and Node 27+ are excluded. Use `@fluojs/platform-bun`, `@fluojs/platform-deno`, or `@fluojs/platform-cloudflare-workers` for non-Node runtimes instead of importing this Node-specific adapter.
|
|
32
|
+
|
|
33
|
+
The adapter owns a Fastify-backed Node `http` or `https` listener. Keep process-specific values such as ports, certificate material, and hostnames at the application boundary, then pass the final options into the adapter explicitly.
|
|
34
|
+
|
|
26
35
|
## When to Use
|
|
27
36
|
|
|
28
37
|
Use this package when you need a high-performance HTTP adapter for your fluo application. Fastify is known for its low overhead and efficient request handling, making it the recommended choice for production fluo applications requiring high throughput and concurrency.
|
|
@@ -41,12 +50,53 @@ const app = await fluoFactory.create(AppModule, {
|
|
|
41
50
|
await app.listen();
|
|
42
51
|
```
|
|
43
52
|
|
|
44
|
-
`createFastifyAdapter()` defaults to port `3000` and does not read `process.env.PORT`; invalid explicit numeric options such as `port`, `maxBodySize`, `retryDelayMs`, `retryLimit`, and `shutdownTimeoutMs` throw during adapter setup. `maxBodySize` and `shutdownTimeoutMs` are non-negative integer byte/time limits, so `0` is valid: `maxBodySize: 0` allows only empty request bodies, and `shutdownTimeoutMs: 0`
|
|
53
|
+
`createFastifyAdapter()` defaults to port `3000` and does not read `process.env.PORT`; invalid explicit numeric options such as `port`, `maxBodySize`, `retryDelayMs`, `retryLimit`, and `shutdownTimeoutMs` throw during adapter setup. `maxBodySize` and `shutdownTimeoutMs` are non-negative integer byte/time limits, so `0` is valid: `maxBodySize: 0` allows only empty request bodies, and `shutdownTimeoutMs: 0` starts Fastify close immediately. The zero value bounds only the wait: if close has not settled, the wait may time out on the next timer turn while the underlying Fastify close and cleanup continue.
|
|
45
54
|
|
|
46
55
|
## Common Patterns
|
|
47
56
|
|
|
57
|
+
### Early Hints
|
|
58
|
+
|
|
59
|
+
Fastify responses expose the optional `context.response.earlyHints` capability through `reply.raw`. Await one `write(...)` per `103`; multiple informational responses precede the independently configured final response. Early fields never populate Fastify final headers or mark the Fluo facade committed. Late/native failures and client disconnects reject deterministically.
|
|
60
|
+
|
|
61
|
+
### Byte Ranges and Cache Validation
|
|
62
|
+
|
|
63
|
+
Fastify preserves the shared `@fluojs/http` single-byte-range and `If-Range` contract. After conditional-request evaluation selects cache validators, a valid `Range: bytes=` request yields the portable `206` identity-byte response; `If-Range` reuses those selected validators, while malformed or multi-range fields retain the full response and an unsatisfiable range yields bodyless `416`. `HEAD` mirrors GET metadata without consuming a stream.
|
|
64
|
+
|
|
65
|
+
### HTTPS/TLS Startup
|
|
66
|
+
When the Fastify process owns TLS directly, pass Node.js `https.ServerOptions` through the `https` option on `createFastifyAdapter(...)`, `bootstrapFastifyApplication(...)`, or `runFastifyApplication(...)`. The adapter starts Fastify with an HTTPS listener, and startup logs report the `https://host:port` URL.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
const app = await fluoFactory.create(AppModule, {
|
|
70
|
+
adapter: createFastifyAdapter({
|
|
71
|
+
host: '0.0.0.0',
|
|
72
|
+
port: 3443,
|
|
73
|
+
https: {
|
|
74
|
+
cert: tlsCertificate,
|
|
75
|
+
key: tlsPrivateKey,
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await app.listen();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Load certificates from your application configuration or secret-management boundary before constructing the adapter; the package does not read certificate files, `process.env`, or `PORT` by itself. If a load balancer, ingress, or API gateway terminates TLS, leave `https` unset and run the Fastify adapter as plain HTTP behind that infrastructure.
|
|
84
|
+
|
|
85
|
+
`bootstrapFastifyApplication(...)` and `runFastifyApplication(...)` accept the same `https`, `host`, and `port` options. `runFastifyApplication(...)` starts listening before it resolves, installs shutdown registration, and returns the running application shell:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
const app = await runFastifyApplication(AppModule, {
|
|
89
|
+
host: '127.0.0.1',
|
|
90
|
+
https: {
|
|
91
|
+
cert: tlsCertificate,
|
|
92
|
+
key: tlsPrivateKey,
|
|
93
|
+
},
|
|
94
|
+
port: 3443,
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
48
98
|
### Multipart and Raw Body
|
|
49
|
-
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`.
|
|
99
|
+
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. Multipart files are attached to the runtime-neutral `FrameworkRequest.files` seam as adapter-provided values; Fastify requests populate it with fluo `UploadedFile` objects after body materialization. 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`.
|
|
50
100
|
|
|
51
101
|
Raw-body capture is skipped for multipart requests, including mixed-case `Content-Type` media values such as `Multipart/Form-Data`. When `multipart.maxTotalSize` is omitted, it defaults to `maxBodySize` so size limits stay portable across HTTP adapters.
|
|
52
102
|
|
|
@@ -62,6 +112,14 @@ const adapter = createFastifyAdapter(
|
|
|
62
112
|
);
|
|
63
113
|
```
|
|
64
114
|
|
|
115
|
+
### Native Raw Request and Response Objects
|
|
116
|
+
|
|
117
|
+
Use the portable `FrameworkRequest` and `FrameworkResponse` fields and methods on `RequestContext` by default. Read request data through `context.request`, then write status, headers, redirects, and bodies through `context.response`; this keeps controllers portable across fluo HTTP adapters.
|
|
118
|
+
|
|
119
|
+
Fastify's shared `raw` fields are intentionally asymmetric: `context.request.raw` is the underlying Node.js `IncomingMessage`, while `context.response.raw` is Fastify's `FastifyReply`. The request raw object is not a `FastifyRequest`, and the response raw object is not a Node.js `ServerResponse`.
|
|
120
|
+
|
|
121
|
+
Do not rely on unsafe casts of those `unknown` fields when migrating NestJS `@Req()` or `@Res()` code. This adapter does not currently expose a typed Fastify-native request accessor. If a portable framework operation cannot satisfy a native Fastify requirement, request a contract-preserving typed accessor rather than coupling shared controller code to a cast.
|
|
122
|
+
|
|
65
123
|
### Server-Backed Real-Time
|
|
66
124
|
Fastify provides a `server-backed` capability that allows `@fluojs/websockets` to attach directly to the underlying Node.js HTTP server.
|
|
67
125
|
|
|
@@ -123,12 +181,35 @@ await bootstrapFastifyApplication(AppModule, {
|
|
|
123
181
|
});
|
|
124
182
|
```
|
|
125
183
|
|
|
184
|
+
### Native Fastify Configuration
|
|
185
|
+
Use portable fluo middleware by default. When a migration must retain a Fastify-native plugin, hook, or instance customization, configure it through the construction-time `configureFastify` seam:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const adapter = createFastifyAdapter({
|
|
189
|
+
configureFastify: async (fastify) => {
|
|
190
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
191
|
+
reply.header('x-native-request-id', request.id);
|
|
192
|
+
});
|
|
193
|
+
fastify.setReplySerializer((payload) => JSON.stringify(payload) ?? '');
|
|
194
|
+
},
|
|
195
|
+
port: 3000,
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`configureFastify` runs once for each Fastify instance that the adapter creates, before fluo registers its multipart, raw-body, native-route, and wildcard-route handling. `bootstrapFastifyApplication(...)` and `runFastifyApplication(...)` accept the same option. A thrown or rejected configuration prevents that `listen()` call from starting; the failed instance is not configured again, while a later `listen()` after a successful `close()` configures the newly created instance once. Although this seam can call `setReplySerializer(...)`, the adapter serializes fluo response payloads before handing them to Fastify, so an instance serializer does not customize fluo responses.
|
|
200
|
+
|
|
201
|
+
The adapter continues to own routing, CORS, logging, multipart and raw-body behavior, response semantics, and shutdown. Do not retain or mutate the Fastify instance after bootstrap, adopt an existing Fastify instance, or use this hook as a native-route bypass. Move portable request behavior to fluo `middleware` instead.
|
|
202
|
+
|
|
126
203
|
### Native Route Registration with Safe Fallback
|
|
127
204
|
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.
|
|
128
205
|
|
|
129
206
|
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.
|
|
130
207
|
|
|
131
|
-
|
|
208
|
+
Validated custom methods such as `QUERY` and `PURGE` also stay on wildcard fallback dispatch instead of receiving native fluo route handoffs. Fastify must know a method name before its wildcard route can accept that request, so the adapter registers descriptor custom methods with Fastify as body-bearing methods while still leaving route selection to fluo. `ALL` is never registered as a wire method, and `CONNECT` remains outside ordinary controller routing conformance.
|
|
209
|
+
|
|
210
|
+
The adapter keeps a wildcard fallback route for unmatched paths and portability-sensitive cases, including multipart requests that must preserve the shared body/materialization path, and enables Fastify trailing-slash / duplicate-slash normalization so native selection stays aligned with fluo's documented route path contract. CORS handling remains owned by fluo's shared middleware path rather than Fastify plugins, and unsupported methods such as `OPTIONS` continue through the fallback dispatcher path unless a fluo route explicitly owns them.
|
|
211
|
+
|
|
212
|
+
Concurrent `listen()` calls share one startup promise and preserve the dispatcher from the first call. After startup, repeated `listen()` calls are no-ops that keep the live listener and dispatcher unchanged. A `listen()` call made while `close()` is in flight waits for shutdown to settle, starts a fresh listener, and resolves only after that listener is ready. Calling `close()` while startup is retrying a busy port cancels the retry loop and waits for it to settle before reporting shutdown completion, so a closed adapter cannot bind later after the caller believes shutdown finished. If an adapter instance is listened again after close, native route handlers refresh their dispatcher descriptors before serving traffic so request handoff metadata cannot point at a previous application graph.
|
|
132
213
|
|
|
133
214
|
## Performance
|
|
134
215
|
|
|
@@ -143,15 +224,15 @@ fluo's Fastify adapter significantly outperforms the raw Node.js adapter in high
|
|
|
143
224
|
|
|
144
225
|
## Conformance Coverage
|
|
145
226
|
|
|
146
|
-
`packages/platform-fastify/src/adapter.test.ts` is the package-local regression target for the documented Fastify adapter contract. It runs the shared `createHttpAdapterPortabilityHarness(...)` checks for malformed cookie preservation, JSON/text raw-body capture, byte-exact raw-body capture, multipart raw-body exclusion, multipart total-size defaults, SSE framing, response stream drain settlement, host and HTTPS startup logging, and shutdown signal listener cleanup.
|
|
227
|
+
`packages/platform-fastify/src/adapter.test.ts` is the package-local regression target for the documented Fastify adapter contract. It runs the shared `createHttpAdapterPortabilityHarness(...)` checks for conditional requests, single-byte ranges and `If-Range`, custom `QUERY`/extension-method fallback, malformed cookie preservation, JSON/text raw-body capture, byte-exact raw-body capture, multipart raw-body exclusion, multipart total-size defaults, SSE framing, response stream drain settlement, host and HTTPS startup logging, and shutdown signal listener cleanup.
|
|
147
228
|
|
|
148
|
-
The same file also covers Fastify-specific native route registration with wildcard fallback, duplicate shape route fallback, middleware/guard/interceptor/observer ordering, CORS ownership, global prefix behavior, malformed cookie preservation, response serialization parity, raw-body pre-parsing behavior, case-insensitive multipart detection, and multipart limit handling. Keep README example pointers aligned with that test file and the custom adapter book chapter when changing startup, routing, or adapter portability behavior.
|
|
229
|
+
The same file also covers Fastify-specific native route registration with wildcard fallback, duplicate shape route fallback, concurrent and repeated `listen()` idempotency, startup retry cancellation during shutdown, native descriptor refresh on adapter reuse, explicit `OPTIONS` route ownership, middleware/guard/interceptor/observer ordering, CORS ownership, global prefix behavior, malformed cookie preservation, response serialization parity, raw-body pre-parsing behavior, zero-valued body/shutdown limits, close wait timeouts that leave the underlying Fastify close in flight, case-insensitive multipart detection, and multipart limit handling. Keep README example pointers aligned with that test file and the custom adapter book chapter when changing startup, routing, or adapter portability behavior.
|
|
149
230
|
|
|
150
231
|
## Public API Overview
|
|
151
232
|
|
|
152
|
-
- `createFastifyAdapter(options, multipartOptions?)`: Recommended factory for the Fastify adapter. The optional second argument configures multipart limits such as `maxFileSize`, `maxFiles`, and `maxTotalSize` for direct adapter construction.
|
|
153
|
-
- `bootstrapFastifyApplication(module, options)`: advanced bootstrap without implicit listening.
|
|
154
|
-
- `runFastifyApplication(module, options)`:
|
|
233
|
+
- `createFastifyAdapter(options, multipartOptions?)`: Recommended factory for the Fastify adapter. `options` includes transport startup knobs such as `host`, `port`, and Node.js `https` server options. The optional second argument configures multipart limits such as `maxFileSize`, `maxFiles`, and `maxTotalSize` for direct adapter construction.
|
|
234
|
+
- `bootstrapFastifyApplication(module, options)`: advanced bootstrap without implicit listening; accepts the same Fastify startup options, including `https`, when the host wants to construct the app before binding it.
|
|
235
|
+
- `runFastifyApplication(module, options)`: Bootstraps the application, starts listening, installs shutdown registration, and returns the running shell with the same `https` startup surface. On signal-driven shutdown timeout/failure it reports the condition through logging and `process.exitCode`, while leaving final process termination to the surrounding host.
|
|
155
236
|
- `isFastifyMultipartTooLargeError(error)`: Detects multipart limit errors across Fastify error shapes.
|
|
156
237
|
- `FastifyHttpApplicationAdapter`: The core adapter implementation.
|
|
157
238
|
- Option types: `FastifyAdapterOptions`, `BootstrapFastifyApplicationOptions`, `RunFastifyApplicationOptions`, `CorsInput`, `FastifyApplicationSignal`.
|
|
@@ -163,6 +244,14 @@ The same file also covers Fastify-specific native route registration with wildca
|
|
|
163
244
|
- **Logging**: The native Fastify logger is disabled to prevent duplicate log streams. `runFastifyApplication` and `bootstrapFastifyApplication` select the framework console logger by default and accept `logger` for hosts or tests that need an injected `ApplicationLogger`.
|
|
164
245
|
- **Global Prefix**: Use `globalPrefixExclude` to prevent the prefix from being applied to internal routes or health check endpoints.
|
|
165
246
|
- **Malformed Cookies**: Malformed cookie headers are preserved rather than failing the request.
|
|
247
|
+
- **HTTPS startup**: Use Node.js `>=24.0.0 <27` and pass certificate material under the adapter `https` option when the Fastify process owns TLS. If TLS is terminated by infrastructure, keep the adapter on plain HTTP behind that boundary.
|
|
248
|
+
- **Startup and shutdown failures**: When startup and Fastify `onClose` both fail, callers receive the original startup rejection with the close failure in `cause` only when `cause` can be read, written, and read back. Otherwise, callers receive a startup-first `AggregateError` whose `errors[0]` is the startup rejection and whose `errors[1]` is the close failure.
|
|
249
|
+
|
|
250
|
+
## Multipart streaming
|
|
251
|
+
|
|
252
|
+
Set `multipart: { strategy: 'stream' }` when bootstrapping Fastify to expose multipart parts through `RequestContext.request.body` as an `AsyncIterable`. Fastify creates the iterator without pre-reading or buffering it; consuming a file part pulls its bytes on demand. In this mode file parts are not materialized as `UploadedFile` values in `request.files`. Buffered multipart parsing remains the default and cannot be combined with stream consumption for the same request body.
|
|
253
|
+
|
|
254
|
+
Runtime route dispatch owns an iterator created for a route and automatically calls `return()` after the handler finishes, cancelling and releasing an active source. Standalone `parseMultipartStream(...)` consumers own that responsibility: consume the iterator to completion or call `return()` when ending early.
|
|
166
255
|
|
|
167
256
|
## Related Packages
|
|
168
257
|
|
package/dist/adapter.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { ServerOptions as HttpsServerOptions } from 'node:https';
|
|
2
2
|
import { type CorsOptions, type Dispatcher, type HttpApplicationAdapter, type MiddlewareLike, type SecurityHeadersOptions } from '@fluojs/http';
|
|
3
|
-
import type { Application, ApplicationLogger, CreateApplicationOptions, ModuleType, MultipartOptions
|
|
4
|
-
|
|
5
|
-
interface FrameworkRequest {
|
|
6
|
-
files?: UploadedFile[];
|
|
7
|
-
rawBody?: Uint8Array;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
3
|
+
import type { Application, ApplicationLogger, CreateApplicationOptions, ModuleType, MultipartOptions } from '@fluojs/runtime';
|
|
4
|
+
import { type FastifyInstance } from 'fastify';
|
|
10
5
|
/**
|
|
11
6
|
* Transport-level knobs for the standalone Fastify HTTP adapter factory.
|
|
12
7
|
*/
|
|
13
8
|
export interface FastifyAdapterOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Configures the internally created Fastify instance before fluo registers
|
|
11
|
+
* its multipart, raw-body, and route handling integrations.
|
|
12
|
+
*/
|
|
13
|
+
configureFastify?: (app: FastifyInstance) => void | Promise<void>;
|
|
14
14
|
host?: string;
|
|
15
15
|
https?: HttpsServerOptions;
|
|
16
16
|
maxBodySize?: number;
|
|
@@ -29,6 +29,7 @@ export type CorsInput = false | string | string[] | CorsOptions;
|
|
|
29
29
|
* implicitly registering process shutdown listeners.
|
|
30
30
|
*/
|
|
31
31
|
export interface BootstrapFastifyApplicationOptions extends Omit<CreateApplicationOptions, 'adapter' | 'logger' | 'middleware'> {
|
|
32
|
+
configureFastify?: FastifyAdapterOptions['configureFastify'];
|
|
32
33
|
cors?: CorsInput;
|
|
33
34
|
globalPrefix?: string;
|
|
34
35
|
globalPrefixExclude?: readonly string[];
|
|
@@ -72,19 +73,30 @@ export declare class FastifyHttpApplicationAdapter implements HttpApplicationAda
|
|
|
72
73
|
private readonly maxBodySize;
|
|
73
74
|
private readonly preserveRawBody;
|
|
74
75
|
private readonly shutdownTimeoutMs;
|
|
76
|
+
private readonly configureFastify?;
|
|
75
77
|
private closeInFlight?;
|
|
78
|
+
private fastifyConfigurationInFlight?;
|
|
76
79
|
private dispatcher?;
|
|
80
|
+
private appClosed;
|
|
81
|
+
private listenAbortController?;
|
|
82
|
+
private listenInFlight?;
|
|
83
|
+
private listenState;
|
|
84
|
+
private readonly nativeRouteDescriptors;
|
|
77
85
|
private pluginsReady;
|
|
78
|
-
private
|
|
86
|
+
private app;
|
|
79
87
|
private readonly requestResponseFactory;
|
|
80
|
-
constructor(port: number, host: string | undefined, retryDelayMs: number | undefined, retryLimit: number | undefined, httpsOptions: HttpsServerOptions | undefined, multipartOptions?: MultipartOptions | undefined, maxBodySize?: number, preserveRawBody?: boolean, shutdownTimeoutMs?: number);
|
|
88
|
+
constructor(port: number, host: string | undefined, retryDelayMs: number | undefined, retryLimit: number | undefined, httpsOptions: HttpsServerOptions | undefined, multipartOptions?: MultipartOptions | undefined, maxBodySize?: number, preserveRawBody?: boolean, shutdownTimeoutMs?: number, configureFastify?: ((app: FastifyInstance) => void | Promise<void>) | undefined);
|
|
81
89
|
getServer(): unknown;
|
|
82
90
|
getRealtimeCapability(): import("@fluojs/http").ServerBackedHttpAdapterRealtimeCapability;
|
|
83
91
|
getListenTarget(): FastifyListenTarget;
|
|
84
92
|
listen(dispatcher: Dispatcher): Promise<void>;
|
|
85
93
|
close(): Promise<void>;
|
|
94
|
+
private closeApplication;
|
|
86
95
|
private registerPluginsAndRoutes;
|
|
96
|
+
private configureFastifyInstance;
|
|
97
|
+
private configureNativeRouteDescriptors;
|
|
87
98
|
private registerNativeRoutes;
|
|
99
|
+
private registerCustomHttpMethods;
|
|
88
100
|
private registerWildcardFallbackRoute;
|
|
89
101
|
private listenWithRetry;
|
|
90
102
|
private handleRequest;
|
|
@@ -114,14 +126,14 @@ export declare function createFastifyAdapter(options?: FastifyAdapterOptions, mu
|
|
|
114
126
|
*/
|
|
115
127
|
export declare function bootstrapFastifyApplication(rootModule: ModuleType, options: BootstrapFastifyApplicationOptions): Promise<Application>;
|
|
116
128
|
/**
|
|
117
|
-
* Bootstrap and
|
|
129
|
+
* Bootstrap and start a Fastify-backed application with shutdown registration.
|
|
118
130
|
*
|
|
119
|
-
* This helper
|
|
120
|
-
*
|
|
131
|
+
* This helper creates the adapter, wires the runtime, awaits `listen()`, installs
|
|
132
|
+
* the configured shutdown registration, and only then returns the running application.
|
|
121
133
|
*
|
|
122
134
|
* @param rootModule Root application module compiled by the Fluo runtime.
|
|
123
135
|
* @param options Runtime, adapter, and shutdown registration settings.
|
|
124
|
-
* @returns A
|
|
136
|
+
* @returns A running application shell after listening succeeds and shutdown registration completes.
|
|
125
137
|
*/
|
|
126
138
|
export declare function runFastifyApplication(rootModule: ModuleType, options: RunFastifyApplicationOptions): Promise<Application>;
|
|
127
139
|
/**
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,IAAI,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAKtE,OAAO,EACL,KAAK,WAAW,EAGhB,KAAK,UAAU,EAKf,KAAK,sBAAsB,EAG3B,KAAK,cAAc,EAEnB,KAAK,sBAAsB,EAC5B,MAAM,cAAc,CAAC;AAOtB,OAAO,KAAK,EACV,WAAW,EACX,iBAAiB,EACjB,wBAAwB,EACxB,UAAU,EACV,gBAAgB,
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
package/dist/adapter.js
CHANGED
|
@@ -2,10 +2,11 @@ import { Transform } from 'node:stream';
|
|
|
2
2
|
import multipart from '@fastify/multipart';
|
|
3
3
|
import { createErrorResponse, createServerBackedHttpAdapterRealtimeCapability, HttpException, InternalServerErrorException, PayloadTooLargeException } from '@fluojs/http';
|
|
4
4
|
import { attachFrameworkRequestNativeRouteHandoff, bindRawRequestNativeRouteHandoff, consumeRawRequestNativeRouteHandoff, isRoutePathNormalizationSensitive } from '@fluojs/http/internal';
|
|
5
|
+
import { parseMultipart, parseMultipartStream } from '@fluojs/runtime/web';
|
|
5
6
|
import { bootstrapHttpAdapterApplication, runHttpAdapterApplication } from '@fluojs/runtime/internal/http-adapter';
|
|
6
7
|
import { dispatchWithRequestResponseFactory } from '@fluojs/runtime/internal/request-response-factory';
|
|
7
|
-
import { cloneHeaderValue, createDeferredFrameworkRequestShell, createMemoizedAsyncValue, createMemoizedValue, parseQueryParamsFromSearch, snapshotSimpleQueryRecord, splitRawRequestUrl } from '@fluojs/
|
|
8
|
-
import { createConsoleApplicationLogger, createNodeShutdownSignalRegistration, defaultNodeShutdownSignals } from '@fluojs/
|
|
8
|
+
import { cloneHeaderValue, createDeferredFrameworkRequestShell, createMemoizedAsyncValue, createMemoizedValue, createNodeEarlyHintsCapability, parseQueryParamsFromSearch, snapshotSimpleQueryRecord, splitRawRequestUrl } from '@fluojs/platform-nodejs/internal';
|
|
9
|
+
import { createConsoleApplicationLogger, createNodeShutdownSignalRegistration, defaultNodeShutdownSignals } from '@fluojs/platform-nodejs';
|
|
9
10
|
import fastify from 'fastify';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -38,11 +39,17 @@ const EMPTY_NATIVE_ROUTE_PARAMS = Object.freeze({});
|
|
|
38
39
|
*/
|
|
39
40
|
export class FastifyHttpApplicationAdapter {
|
|
40
41
|
closeInFlight;
|
|
42
|
+
fastifyConfigurationInFlight;
|
|
41
43
|
dispatcher;
|
|
44
|
+
appClosed = false;
|
|
45
|
+
listenAbortController;
|
|
46
|
+
listenInFlight;
|
|
47
|
+
listenState = 'idle';
|
|
48
|
+
nativeRouteDescriptors = new Map();
|
|
42
49
|
pluginsReady = false;
|
|
43
50
|
app;
|
|
44
51
|
requestResponseFactory;
|
|
45
|
-
constructor(port, host, retryDelayMs = 150, retryLimit = 20, httpsOptions, multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false, shutdownTimeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS) {
|
|
52
|
+
constructor(port, host, retryDelayMs = 150, retryLimit = 20, httpsOptions, multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false, shutdownTimeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS, configureFastify) {
|
|
46
53
|
this.port = port;
|
|
47
54
|
this.host = host;
|
|
48
55
|
this.retryDelayMs = retryDelayMs;
|
|
@@ -52,6 +59,7 @@ export class FastifyHttpApplicationAdapter {
|
|
|
52
59
|
this.maxBodySize = maxBodySize;
|
|
53
60
|
this.preserveRawBody = preserveRawBody;
|
|
54
61
|
this.shutdownTimeoutMs = shutdownTimeoutMs;
|
|
62
|
+
this.configureFastify = configureFastify;
|
|
55
63
|
resolvePort(this.port);
|
|
56
64
|
resolveNonNegativeIntegerOption('retryDelayMs', this.retryDelayMs, 150);
|
|
57
65
|
resolveNonNegativeIntegerOption('retryLimit', this.retryLimit, 20);
|
|
@@ -69,49 +77,146 @@ export class FastifyHttpApplicationAdapter {
|
|
|
69
77
|
getListenTarget() {
|
|
70
78
|
return resolveListenTarget(this.app.server.address() ?? null, this.port, this.host, this.httpsOptions !== undefined);
|
|
71
79
|
}
|
|
72
|
-
|
|
73
|
-
this.dispatcher = dispatcher;
|
|
74
|
-
await this.registerPluginsAndRoutes(dispatcher);
|
|
75
|
-
await this.listenWithRetry();
|
|
76
|
-
}
|
|
77
|
-
async close() {
|
|
80
|
+
listen(dispatcher) {
|
|
78
81
|
if (this.closeInFlight) {
|
|
79
|
-
|
|
80
|
-
return;
|
|
82
|
+
return this.closeInFlight.then(() => this.listen(dispatcher));
|
|
81
83
|
}
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
return;
|
|
84
|
+
if (this.listenState === 'listening') {
|
|
85
|
+
return Promise.resolve();
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
if (this.listenInFlight) {
|
|
88
|
+
return this.listenInFlight;
|
|
89
|
+
}
|
|
90
|
+
if (this.appClosed) {
|
|
91
|
+
this.app = createFastifyApp(this.httpsOptions, this.maxBodySize);
|
|
92
|
+
this.appClosed = false;
|
|
93
|
+
this.fastifyConfigurationInFlight = undefined;
|
|
94
|
+
this.pluginsReady = false;
|
|
95
|
+
}
|
|
96
|
+
this.dispatcher = dispatcher;
|
|
97
|
+
this.configureNativeRouteDescriptors(dispatcher);
|
|
98
|
+
const abortController = new AbortController();
|
|
99
|
+
this.listenAbortController = abortController;
|
|
100
|
+
this.listenState = 'starting';
|
|
101
|
+
const listenInFlight = this.registerPluginsAndRoutes(dispatcher).then(() => this.listenWithRetry(abortController.signal)).then(() => {
|
|
102
|
+
this.listenState = 'listening';
|
|
103
|
+
}, error => {
|
|
104
|
+
this.listenState = 'idle';
|
|
105
|
+
throw error;
|
|
106
|
+
}).finally(() => {
|
|
107
|
+
if (this.listenInFlight === listenInFlight) {
|
|
108
|
+
this.listenInFlight = undefined;
|
|
109
|
+
}
|
|
110
|
+
if (this.listenAbortController === abortController) {
|
|
111
|
+
this.listenAbortController = undefined;
|
|
112
|
+
}
|
|
90
113
|
});
|
|
91
|
-
this.
|
|
92
|
-
|
|
93
|
-
|
|
114
|
+
this.listenInFlight = listenInFlight;
|
|
115
|
+
return listenInFlight;
|
|
116
|
+
}
|
|
117
|
+
close() {
|
|
118
|
+
if (!this.closeInFlight) {
|
|
119
|
+
const closeInFlight = this.closeApplication().finally(() => {
|
|
120
|
+
if (this.closeInFlight === closeInFlight) {
|
|
121
|
+
this.closeInFlight = undefined;
|
|
122
|
+
}
|
|
123
|
+
this.listenState = 'idle';
|
|
124
|
+
this.dispatcher = undefined;
|
|
125
|
+
this.nativeRouteDescriptors.clear();
|
|
126
|
+
});
|
|
127
|
+
this.closeInFlight = closeInFlight;
|
|
128
|
+
void closeInFlight.catch(() => {});
|
|
129
|
+
}
|
|
130
|
+
return waitForCloseWithTimeout(this.closeInFlight, this.shutdownTimeoutMs);
|
|
131
|
+
}
|
|
132
|
+
async closeApplication() {
|
|
133
|
+
let startupError;
|
|
134
|
+
let startupFailed = false;
|
|
135
|
+
let closeError;
|
|
136
|
+
let closeFailed = false;
|
|
137
|
+
try {
|
|
138
|
+
if (this.listenInFlight) {
|
|
139
|
+
this.listenAbortController?.abort();
|
|
140
|
+
await ignoreCancelledListen(this.listenInFlight);
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
startupError = error;
|
|
144
|
+
startupFailed = true;
|
|
145
|
+
} finally {
|
|
146
|
+
if (!this.appClosed) {
|
|
147
|
+
try {
|
|
148
|
+
await this.app.close();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
closeError = error;
|
|
151
|
+
closeFailed = true;
|
|
152
|
+
} finally {
|
|
153
|
+
this.appClosed = true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (startupFailed) {
|
|
158
|
+
if (closeFailed) {
|
|
159
|
+
if (typeof startupError === 'object' && startupError !== null || typeof startupError === 'function') {
|
|
160
|
+
try {
|
|
161
|
+
const existingCause = Reflect.get(startupError, 'cause');
|
|
162
|
+
const closeFailure = existingCause === undefined ? closeError : new AggregateError([existingCause, closeError], 'Fastify startup and shutdown both failed.');
|
|
163
|
+
const causeAttached = Reflect.set(startupError, 'cause', closeFailure);
|
|
164
|
+
if (!causeAttached || Reflect.get(startupError, 'cause') !== closeFailure) {
|
|
165
|
+
startupError = new AggregateError([startupError, closeError], 'Fastify startup and shutdown both failed.');
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
// Mutable rejection objects preserve identity only when cause can be read, written, and
|
|
169
|
+
// read back. Every other rejection value uses startup-first errors to expose both failures.
|
|
170
|
+
startupError = new AggregateError([startupError, closeError], 'Fastify startup and shutdown both failed.');
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
startupError = new AggregateError([startupError, closeError], 'Fastify startup and shutdown both failed.');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
throw startupError;
|
|
177
|
+
}
|
|
178
|
+
if (closeFailed) {
|
|
179
|
+
throw closeError;
|
|
180
|
+
}
|
|
94
181
|
}
|
|
95
182
|
async registerPluginsAndRoutes(dispatcher) {
|
|
96
183
|
if (this.pluginsReady) {
|
|
97
184
|
return;
|
|
98
185
|
}
|
|
186
|
+
await this.configureFastifyInstance();
|
|
99
187
|
await this.app.register(multipart);
|
|
100
188
|
if (this.preserveRawBody) {
|
|
101
189
|
this.app.addHook('preParsing', captureRawBodyPreParsingHook);
|
|
102
190
|
}
|
|
103
|
-
|
|
191
|
+
const descriptors = resolveDispatcherRouteDescriptors(dispatcher);
|
|
192
|
+
this.registerCustomHttpMethods(descriptors);
|
|
193
|
+
this.registerNativeRoutes(descriptors);
|
|
104
194
|
this.registerWildcardFallbackRoute();
|
|
105
195
|
this.pluginsReady = true;
|
|
106
196
|
}
|
|
197
|
+
configureFastifyInstance() {
|
|
198
|
+
if (!this.fastifyConfigurationInFlight) {
|
|
199
|
+
this.fastifyConfigurationInFlight = Promise.resolve().then(() => this.configureFastify?.(this.app)).catch(error => {
|
|
200
|
+
throw error;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return this.fastifyConfigurationInFlight;
|
|
204
|
+
}
|
|
205
|
+
configureNativeRouteDescriptors(dispatcher) {
|
|
206
|
+
this.nativeRouteDescriptors.clear();
|
|
207
|
+
for (const route of createFastifyNativeRoutes(resolveDispatcherRouteDescriptors(dispatcher))) {
|
|
208
|
+
this.nativeRouteDescriptors.set(route.routeKey, route.descriptor);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
107
211
|
registerNativeRoutes(descriptors) {
|
|
108
212
|
for (const route of createFastifyNativeRoutes(descriptors)) {
|
|
109
213
|
this.app.route({
|
|
110
214
|
handler: async (request, reply) => {
|
|
111
215
|
const urlParts = splitRawRequestUrl(request.raw.url ?? '/');
|
|
112
216
|
const params = normalizeNativeRouteParams(request.params);
|
|
113
|
-
|
|
114
|
-
|
|
217
|
+
const descriptor = this.nativeRouteDescriptors.get(route.routeKey);
|
|
218
|
+
if (descriptor && !isRoutePathNormalizationSensitive(urlParts.path) && !hasNativeRouteParamSeparators(params)) {
|
|
219
|
+
await this.handleNativeRouteRequest(descriptor, params, urlParts, request, reply);
|
|
115
220
|
return;
|
|
116
221
|
}
|
|
117
222
|
await this.handleRequest(request, reply);
|
|
@@ -121,24 +226,37 @@ export class FastifyHttpApplicationAdapter {
|
|
|
121
226
|
});
|
|
122
227
|
}
|
|
123
228
|
}
|
|
229
|
+
registerCustomHttpMethods(descriptors) {
|
|
230
|
+
for (const descriptor of descriptors) {
|
|
231
|
+
const method = descriptor.route.method;
|
|
232
|
+
if (method === 'ALL' || method === 'CONNECT' || this.app.supportedMethods.includes(method)) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
this.app.addHttpMethod(method, {
|
|
236
|
+
hasBody: true
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
124
240
|
registerWildcardFallbackRoute() {
|
|
125
241
|
this.app.all('*', async (request, reply) => {
|
|
126
242
|
await this.handleRequest(request, reply);
|
|
127
243
|
});
|
|
128
244
|
}
|
|
129
|
-
async listenWithRetry() {
|
|
245
|
+
async listenWithRetry(signal) {
|
|
130
246
|
for (let attempt = 0;; attempt++) {
|
|
247
|
+
throwIfListenCancelled(signal);
|
|
131
248
|
try {
|
|
132
249
|
await this.app.listen({
|
|
133
250
|
host: this.host,
|
|
134
251
|
port: this.port
|
|
135
252
|
});
|
|
253
|
+
throwIfListenCancelled(signal);
|
|
136
254
|
return;
|
|
137
255
|
} catch (error) {
|
|
138
256
|
if (!isAddressInUseError(error) || attempt >= this.retryLimit) {
|
|
139
257
|
throw error;
|
|
140
258
|
}
|
|
141
|
-
await delay(this.retryDelayMs);
|
|
259
|
+
await delay(this.retryDelayMs, signal);
|
|
142
260
|
}
|
|
143
261
|
}
|
|
144
262
|
}
|
|
@@ -209,6 +327,7 @@ function createNativeFastFrameworkRequest(request, lazySignal, urlParts, maxBody
|
|
|
209
327
|
if (Number.isFinite(contentLength) && contentLength > maxBodySize) {
|
|
210
328
|
throw new PayloadTooLargeException('Request body exceeds the size limit.');
|
|
211
329
|
}
|
|
330
|
+
assertBodyWithinMaxBodySize(request.body, maxBodySize);
|
|
212
331
|
const frameworkRequest = createDeferredFrameworkRequestShell({
|
|
213
332
|
cookieHeader: cloneHeaderValue(request.headers.cookie),
|
|
214
333
|
headersFactory: () => normalizeHeaders(cloneRequestHeaders(request.headers)),
|
|
@@ -314,11 +433,13 @@ function createFastifyNativeRoutes(descriptors) {
|
|
|
314
433
|
return [...candidates.values()].filter(candidate => shapePaths.get(candidate.shapeKey)?.size === 1).map(({
|
|
315
434
|
descriptor,
|
|
316
435
|
method,
|
|
317
|
-
path
|
|
436
|
+
path,
|
|
437
|
+
routeKey
|
|
318
438
|
}) => ({
|
|
319
439
|
descriptor,
|
|
320
440
|
method,
|
|
321
|
-
path
|
|
441
|
+
path,
|
|
442
|
+
routeKey
|
|
322
443
|
}));
|
|
323
444
|
}
|
|
324
445
|
function isFastifyNativeRouteDescriptor(descriptor) {
|
|
@@ -334,6 +455,7 @@ function registerFastifyNativeRouteCandidate(candidates, shapePaths, descriptor)
|
|
|
334
455
|
descriptor,
|
|
335
456
|
method,
|
|
336
457
|
path,
|
|
458
|
+
routeKey,
|
|
337
459
|
shapeKey
|
|
338
460
|
});
|
|
339
461
|
}
|
|
@@ -364,7 +486,7 @@ function canonicalizeFastifyRouteShape(path) {
|
|
|
364
486
|
* @returns A runtime `HttpApplicationAdapter` backed by Fastify.
|
|
365
487
|
*/
|
|
366
488
|
export function createFastifyAdapter(options = {}, multipartOptions) {
|
|
367
|
-
return new FastifyHttpApplicationAdapter(resolvePort(options.port), options.host, options.retryDelayMs, options.retryLimit, options.https, multipartOptions, options.maxBodySize, options.rawBody, options.shutdownTimeoutMs);
|
|
489
|
+
return new FastifyHttpApplicationAdapter(resolvePort(options.port), options.host, options.retryDelayMs, options.retryLimit, options.https, multipartOptions, options.maxBodySize, options.rawBody, options.shutdownTimeoutMs, options.configureFastify);
|
|
368
490
|
}
|
|
369
491
|
|
|
370
492
|
/**
|
|
@@ -380,14 +502,14 @@ export async function bootstrapFastifyApplication(rootModule, options) {
|
|
|
380
502
|
}
|
|
381
503
|
|
|
382
504
|
/**
|
|
383
|
-
* Bootstrap and
|
|
505
|
+
* Bootstrap and start a Fastify-backed application with shutdown registration.
|
|
384
506
|
*
|
|
385
|
-
* This helper
|
|
386
|
-
*
|
|
507
|
+
* This helper creates the adapter, wires the runtime, awaits `listen()`, installs
|
|
508
|
+
* the configured shutdown registration, and only then returns the running application.
|
|
387
509
|
*
|
|
388
510
|
* @param rootModule Root application module compiled by the Fluo runtime.
|
|
389
511
|
* @param options Runtime, adapter, and shutdown registration settings.
|
|
390
|
-
* @returns A
|
|
512
|
+
* @returns A running application shell after listening succeeds and shutdown registration completes.
|
|
391
513
|
*/
|
|
392
514
|
export async function runFastifyApplication(rootModule, options) {
|
|
393
515
|
const logger = options.logger ?? createConsoleApplicationLogger();
|
|
@@ -399,6 +521,7 @@ export async function runFastifyApplication(rootModule, options) {
|
|
|
399
521
|
}
|
|
400
522
|
class MutableFastifyFrameworkResponse {
|
|
401
523
|
committed;
|
|
524
|
+
earlyHints;
|
|
402
525
|
headers = {};
|
|
403
526
|
raw;
|
|
404
527
|
statusCode;
|
|
@@ -407,6 +530,7 @@ class MutableFastifyFrameworkResponse {
|
|
|
407
530
|
constructor(reply) {
|
|
408
531
|
this.reply = reply;
|
|
409
532
|
this.committed = reply.sent;
|
|
533
|
+
this.earlyHints = createNodeEarlyHintsCapability(reply.raw, () => this.committed);
|
|
410
534
|
this.raw = reply;
|
|
411
535
|
}
|
|
412
536
|
get stream() {
|
|
@@ -419,11 +543,14 @@ class MutableFastifyFrameworkResponse {
|
|
|
419
543
|
this.committed = true;
|
|
420
544
|
this.reply.redirect(location, status);
|
|
421
545
|
}
|
|
422
|
-
send(body) {
|
|
546
|
+
send(body, options) {
|
|
423
547
|
if (this.reply.sent) {
|
|
424
548
|
this.committed = true;
|
|
425
549
|
return;
|
|
426
550
|
}
|
|
551
|
+
if (options?.compression === false) {
|
|
552
|
+
disableNativeCompression(this.reply);
|
|
553
|
+
}
|
|
427
554
|
const existingContentType = this.reply.getHeader('content-type');
|
|
428
555
|
const serialized = serializeResponseBody(body, typeof existingContentType === 'string' ? existingContentType : undefined);
|
|
429
556
|
if (!this.reply.hasHeader('content-type') && serialized.defaultContentType) {
|
|
@@ -446,9 +573,8 @@ class MutableFastifyFrameworkResponse {
|
|
|
446
573
|
setHeader(name, value) {
|
|
447
574
|
const lowerName = name.toLowerCase();
|
|
448
575
|
if (lowerName === 'set-cookie') {
|
|
449
|
-
|
|
450
|
-
this.
|
|
451
|
-
this.headers[name] = merged;
|
|
576
|
+
this.reply.header(name, value);
|
|
577
|
+
this.headers[name] = mergeSetCookieHeader(this.headers[name], value);
|
|
452
578
|
return;
|
|
453
579
|
}
|
|
454
580
|
this.reply.header(name, value);
|
|
@@ -460,6 +586,13 @@ class MutableFastifyFrameworkResponse {
|
|
|
460
586
|
this.statusSet = true;
|
|
461
587
|
}
|
|
462
588
|
}
|
|
589
|
+
function disableNativeCompression(reply) {
|
|
590
|
+
const cacheControl = reply.getHeader('cache-control');
|
|
591
|
+
const value = Array.isArray(cacheControl) ? cacheControl.join(', ') : String(cacheControl ?? '');
|
|
592
|
+
if (!/\bno-transform\b/i.test(value)) {
|
|
593
|
+
reply.header('Cache-Control', value ? `${value}, no-transform` : 'no-transform');
|
|
594
|
+
}
|
|
595
|
+
}
|
|
463
596
|
function createFrameworkResponse(reply) {
|
|
464
597
|
return new MutableFastifyFrameworkResponse(reply);
|
|
465
598
|
}
|
|
@@ -487,6 +620,9 @@ function createFrameworkResponseStream(reply) {
|
|
|
487
620
|
get closed() {
|
|
488
621
|
return reply.raw.writableEnded;
|
|
489
622
|
},
|
|
623
|
+
disableCompression() {
|
|
624
|
+
disableNativeCompression(reply);
|
|
625
|
+
},
|
|
490
626
|
flush() {
|
|
491
627
|
ensureHijacked();
|
|
492
628
|
reply.raw.flushHeaders?.();
|
|
@@ -497,21 +633,34 @@ function createFrameworkResponseStream(reply) {
|
|
|
497
633
|
reply.raw.removeListener('close', listener);
|
|
498
634
|
};
|
|
499
635
|
},
|
|
636
|
+
onError(listener) {
|
|
637
|
+
reply.raw.on('error', listener);
|
|
638
|
+
return () => {
|
|
639
|
+
reply.raw.removeListener('error', listener);
|
|
640
|
+
};
|
|
641
|
+
},
|
|
500
642
|
waitForDrain() {
|
|
501
643
|
ensureHijacked();
|
|
502
644
|
if (reply.raw.writableEnded || reply.raw.destroyed) {
|
|
503
645
|
return Promise.resolve();
|
|
504
646
|
}
|
|
505
|
-
return new Promise(resolve => {
|
|
506
|
-
const
|
|
507
|
-
reply.raw.removeListener('drain',
|
|
508
|
-
reply.raw.removeListener('close',
|
|
509
|
-
reply.raw.removeListener('error',
|
|
647
|
+
return new Promise((resolve, reject) => {
|
|
648
|
+
const cleanup = () => {
|
|
649
|
+
reply.raw.removeListener('drain', resolveDrain);
|
|
650
|
+
reply.raw.removeListener('close', resolveDrain);
|
|
651
|
+
reply.raw.removeListener('error', rejectError);
|
|
652
|
+
};
|
|
653
|
+
const rejectError = error => {
|
|
654
|
+
cleanup();
|
|
655
|
+
reject(error);
|
|
656
|
+
};
|
|
657
|
+
const resolveDrain = () => {
|
|
658
|
+
cleanup();
|
|
510
659
|
resolve();
|
|
511
660
|
};
|
|
512
|
-
reply.raw.once('drain',
|
|
513
|
-
reply.raw.once('close',
|
|
514
|
-
reply.raw.once('error',
|
|
661
|
+
reply.raw.once('drain', resolveDrain);
|
|
662
|
+
reply.raw.once('close', resolveDrain);
|
|
663
|
+
reply.raw.once('error', rejectError);
|
|
515
664
|
});
|
|
516
665
|
},
|
|
517
666
|
write(chunk) {
|
|
@@ -536,12 +685,23 @@ function createDeferredFrameworkRequest(request, signal, multipartOptions, maxBo
|
|
|
536
685
|
let body = request.body;
|
|
537
686
|
let files;
|
|
538
687
|
if (isMultipart) {
|
|
539
|
-
const
|
|
688
|
+
const resolvedMultipartOptions = {
|
|
540
689
|
...multipartOptions,
|
|
541
690
|
maxTotalSize: multipartOptions?.maxTotalSize ?? maxBodySize
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
|
|
691
|
+
};
|
|
692
|
+
if (multipartOptions?.strategy === 'stream') {
|
|
693
|
+
body = parseMultipartStream({
|
|
694
|
+
body: request.raw,
|
|
695
|
+
headers: headerSnapshot,
|
|
696
|
+
method: request.method,
|
|
697
|
+
signal,
|
|
698
|
+
url: rawUrl
|
|
699
|
+
}, resolvedMultipartOptions);
|
|
700
|
+
} else {
|
|
701
|
+
const parsed = await parseMultipartRequest(request, resolvedMultipartOptions);
|
|
702
|
+
body = parsed.fields;
|
|
703
|
+
files = parsed.files;
|
|
704
|
+
}
|
|
545
705
|
}
|
|
546
706
|
frameworkRequest.body = body;
|
|
547
707
|
if (files) {
|
|
@@ -550,6 +710,7 @@ function createDeferredFrameworkRequest(request, signal, multipartOptions, maxBo
|
|
|
550
710
|
if (preserveRawBody && !isMultipart) {
|
|
551
711
|
const rawBodyValue = request.rawBody;
|
|
552
712
|
if (rawBodyValue !== undefined) {
|
|
713
|
+
assertBodyWithinMaxBodySize(rawBodyValue, maxBodySize);
|
|
553
714
|
frameworkRequest.rawBody = rawBodyValue;
|
|
554
715
|
}
|
|
555
716
|
}
|
|
@@ -568,6 +729,7 @@ function createDeferredFrameworkRequest(request, signal, multipartOptions, maxBo
|
|
|
568
729
|
url: urlParts.path + urlParts.search
|
|
569
730
|
});
|
|
570
731
|
if (!needsDeferredBodyMaterialization) {
|
|
732
|
+
assertBodyWithinMaxBodySize(request.body, maxBodySize);
|
|
571
733
|
frameworkRequest.body = request.body;
|
|
572
734
|
}
|
|
573
735
|
const nativeRouteHandoff = consumeRawRequestNativeRouteHandoff(request.raw);
|
|
@@ -603,6 +765,26 @@ function hasNativeRouteParamSeparators(params) {
|
|
|
603
765
|
}
|
|
604
766
|
return false;
|
|
605
767
|
}
|
|
768
|
+
function assertBodyWithinMaxBodySize(body, maxBodySize) {
|
|
769
|
+
if (resolveFastifyBodySize(body) > maxBodySize) {
|
|
770
|
+
throw new PayloadTooLargeException('Request body exceeds the size limit.');
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function resolveFastifyBodySize(body) {
|
|
774
|
+
if (body === undefined || body === null) {
|
|
775
|
+
return 0;
|
|
776
|
+
}
|
|
777
|
+
if (typeof body === 'string') {
|
|
778
|
+
return Buffer.byteLength(body, 'utf8');
|
|
779
|
+
}
|
|
780
|
+
if (Buffer.isBuffer(body)) {
|
|
781
|
+
return body.byteLength;
|
|
782
|
+
}
|
|
783
|
+
if (body instanceof Uint8Array) {
|
|
784
|
+
return body.byteLength;
|
|
785
|
+
}
|
|
786
|
+
return 1;
|
|
787
|
+
}
|
|
606
788
|
function collectVersionSensitiveRouteKeys(descriptors) {
|
|
607
789
|
const grouped = new Map();
|
|
608
790
|
for (const descriptor of descriptors) {
|
|
@@ -620,59 +802,8 @@ function collectVersionSensitiveRouteKeys(descriptors) {
|
|
|
620
802
|
}
|
|
621
803
|
return new Set([...grouped.entries()].filter(([, current]) => current.count > 1 || current.hasVersioned).map(([routeKey]) => routeKey));
|
|
622
804
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const files = [];
|
|
626
|
-
const maxFileSize = options.maxFileSize ?? 10 * 1024 * 1024;
|
|
627
|
-
const maxFiles = options.maxFiles ?? 10;
|
|
628
|
-
const maxTotalSize = options.maxTotalSize ?? 10 * 1024 * 1024;
|
|
629
|
-
const contentLength = Number(request.headers['content-length']);
|
|
630
|
-
let totalSize = 0;
|
|
631
|
-
if (Number.isFinite(contentLength) && contentLength > maxTotalSize) {
|
|
632
|
-
throw new PayloadTooLargeException('Request body exceeds the configured multipart limits.');
|
|
633
|
-
}
|
|
634
|
-
try {
|
|
635
|
-
for await (const part of request.parts({
|
|
636
|
-
limits: {
|
|
637
|
-
fileSize: maxFileSize,
|
|
638
|
-
files: maxFiles
|
|
639
|
-
}
|
|
640
|
-
})) {
|
|
641
|
-
if (part.type === 'file') {
|
|
642
|
-
if (files.length >= maxFiles) {
|
|
643
|
-
throw new PayloadTooLargeException(`Exceeded maximum file count of ${String(maxFiles)}.`);
|
|
644
|
-
}
|
|
645
|
-
const buffer = await part.toBuffer();
|
|
646
|
-
totalSize += buffer.byteLength;
|
|
647
|
-
if (totalSize > maxTotalSize) {
|
|
648
|
-
throw new PayloadTooLargeException('Request body exceeds the configured multipart limits.');
|
|
649
|
-
}
|
|
650
|
-
files.push({
|
|
651
|
-
buffer,
|
|
652
|
-
fieldname: part.fieldname,
|
|
653
|
-
mimetype: part.mimetype,
|
|
654
|
-
originalname: part.filename,
|
|
655
|
-
size: buffer.byteLength
|
|
656
|
-
});
|
|
657
|
-
continue;
|
|
658
|
-
}
|
|
659
|
-
const value = String(part.value ?? '');
|
|
660
|
-
totalSize += Buffer.byteLength(value, 'utf8');
|
|
661
|
-
if (totalSize > maxTotalSize) {
|
|
662
|
-
throw new PayloadTooLargeException('Request body exceeds the configured multipart limits.');
|
|
663
|
-
}
|
|
664
|
-
setMultiValue(fields, part.fieldname, value);
|
|
665
|
-
}
|
|
666
|
-
} catch (error) {
|
|
667
|
-
if (isFastifyMultipartTooLargeError(error)) {
|
|
668
|
-
throw new PayloadTooLargeException('Request body exceeds the configured multipart limits.');
|
|
669
|
-
}
|
|
670
|
-
throw error;
|
|
671
|
-
}
|
|
672
|
-
return {
|
|
673
|
-
fields,
|
|
674
|
-
files
|
|
675
|
-
};
|
|
805
|
+
function parseMultipartRequest(request, options = {}) {
|
|
806
|
+
return parseMultipart(request.raw, options);
|
|
676
807
|
}
|
|
677
808
|
|
|
678
809
|
/**
|
|
@@ -716,18 +847,6 @@ function normalizeHeaders(headers) {
|
|
|
716
847
|
function cloneRequestHeaders(headers) {
|
|
717
848
|
return Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, cloneHeaderValue(value)]));
|
|
718
849
|
}
|
|
719
|
-
function setMultiValue(target, key, value) {
|
|
720
|
-
const existing = target[key];
|
|
721
|
-
if (existing === undefined) {
|
|
722
|
-
target[key] = value;
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
if (Array.isArray(existing)) {
|
|
726
|
-
existing.push(value);
|
|
727
|
-
return;
|
|
728
|
-
}
|
|
729
|
-
target[key] = [existing, value];
|
|
730
|
-
}
|
|
731
850
|
function createRequestSignal(response) {
|
|
732
851
|
const controller = new AbortController();
|
|
733
852
|
const abort = reason => {
|
|
@@ -749,7 +868,7 @@ function resolveRequestIdFromHeaders(headers) {
|
|
|
749
868
|
function createFastifyApp(httpsOptions, maxBodySize) {
|
|
750
869
|
if (httpsOptions) {
|
|
751
870
|
return fastify({
|
|
752
|
-
bodyLimit: maxBodySize,
|
|
871
|
+
bodyLimit: resolveFastifyBodyLimit(maxBodySize),
|
|
753
872
|
exposeHeadRoutes: false,
|
|
754
873
|
https: httpsOptions,
|
|
755
874
|
logger: false,
|
|
@@ -760,7 +879,7 @@ function createFastifyApp(httpsOptions, maxBodySize) {
|
|
|
760
879
|
});
|
|
761
880
|
}
|
|
762
881
|
return fastify({
|
|
763
|
-
bodyLimit: maxBodySize,
|
|
882
|
+
bodyLimit: resolveFastifyBodyLimit(maxBodySize),
|
|
764
883
|
exposeHeadRoutes: false,
|
|
765
884
|
logger: false,
|
|
766
885
|
routerOptions: {
|
|
@@ -769,6 +888,9 @@ function createFastifyApp(httpsOptions, maxBodySize) {
|
|
|
769
888
|
}
|
|
770
889
|
});
|
|
771
890
|
}
|
|
891
|
+
function resolveFastifyBodyLimit(maxBodySize) {
|
|
892
|
+
return Math.max(maxBodySize, 1);
|
|
893
|
+
}
|
|
772
894
|
function captureRawBodyPreParsingHook(request, _reply, payload, done) {
|
|
773
895
|
if (isMultipartRequestContentType(request.headers['content-type'])) {
|
|
774
896
|
done(null, payload);
|
|
@@ -815,8 +937,11 @@ function createRawBodyBufferChunk(chunk, encoding) {
|
|
|
815
937
|
throw new TypeError(`Fastify raw-body capture received unsupported ${typeof chunk} stream chunk.`);
|
|
816
938
|
}
|
|
817
939
|
function isMultipartRequestContentType(contentType) {
|
|
940
|
+
return normalizePrimaryMediaType(contentType) === 'multipart/form-data';
|
|
941
|
+
}
|
|
942
|
+
function normalizePrimaryMediaType(contentType) {
|
|
818
943
|
const primaryValue = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
819
|
-
return
|
|
944
|
+
return primaryValue?.split(';')[0]?.trim().toLowerCase();
|
|
820
945
|
}
|
|
821
946
|
function resolveListenTarget(address, port, host, useHttps) {
|
|
822
947
|
const protocol = useHttps ? 'https' : 'http';
|
|
@@ -867,9 +992,46 @@ function isAddressInUseError(error) {
|
|
|
867
992
|
}
|
|
868
993
|
return error.code === 'EADDRINUSE';
|
|
869
994
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
995
|
+
class FastifyListenCancelledError extends Error {
|
|
996
|
+
constructor() {
|
|
997
|
+
super('Fastify adapter startup was cancelled during shutdown.');
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function throwIfListenCancelled(signal) {
|
|
1001
|
+
if (signal.aborted) {
|
|
1002
|
+
throw new FastifyListenCancelledError();
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function isFastifyListenCancelledError(error) {
|
|
1006
|
+
return error instanceof FastifyListenCancelledError;
|
|
1007
|
+
}
|
|
1008
|
+
async function ignoreCancelledListen(listenPromise) {
|
|
1009
|
+
try {
|
|
1010
|
+
await listenPromise;
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
if (isFastifyListenCancelledError(error)) {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
throw error;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
function delay(ms, signal) {
|
|
1019
|
+
return new Promise((resolve, reject) => {
|
|
1020
|
+
if (signal?.aborted) {
|
|
1021
|
+
reject(new FastifyListenCancelledError());
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
const timeout = setTimeout(() => {
|
|
1025
|
+
signal?.removeEventListener('abort', onAbort);
|
|
1026
|
+
resolve();
|
|
1027
|
+
}, ms);
|
|
1028
|
+
const onAbort = () => {
|
|
1029
|
+
clearTimeout(timeout);
|
|
1030
|
+
reject(new FastifyListenCancelledError());
|
|
1031
|
+
};
|
|
1032
|
+
signal?.addEventListener('abort', onAbort, {
|
|
1033
|
+
once: true
|
|
1034
|
+
});
|
|
873
1035
|
});
|
|
874
1036
|
}
|
|
875
1037
|
function waitForCloseWithTimeout(closePromise, timeoutMs) {
|
|
@@ -932,5 +1094,6 @@ function serializeResponseBody(body, contentType) {
|
|
|
932
1094
|
};
|
|
933
1095
|
}
|
|
934
1096
|
function isJsonContentType(contentType) {
|
|
935
|
-
|
|
1097
|
+
const normalized = normalizePrimaryMediaType(contentType);
|
|
1098
|
+
return normalized === 'application/json' || normalized?.endsWith('+json') === true;
|
|
936
1099
|
}
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"platform",
|
|
9
9
|
"server"
|
|
10
10
|
],
|
|
11
|
-
"version": "
|
|
11
|
+
"version": "2.0.0",
|
|
12
12
|
"private": false,
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"repository": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"directory": "packages/platform-fastify"
|
|
18
18
|
},
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": ">=
|
|
20
|
+
"node": ">=24.0.0 <27"
|
|
21
21
|
},
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
|
@@ -36,15 +36,16 @@
|
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@fastify/multipart": "^9.2.1",
|
|
39
|
-
"fastify": "^5.
|
|
39
|
+
"fastify": "^5.12.3",
|
|
40
40
|
"fastify-raw-body": "^5.0.0",
|
|
41
|
-
"@fluojs/http": "^
|
|
42
|
-
"@fluojs/
|
|
41
|
+
"@fluojs/http": "^3.0.0",
|
|
42
|
+
"@fluojs/platform-nodejs": "^2.0.0",
|
|
43
|
+
"@fluojs/runtime": "^3.0.0"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
|
-
"vitest": "^
|
|
46
|
-
"@fluojs/di": "^
|
|
47
|
-
"@fluojs/testing": "^
|
|
46
|
+
"vitest": "^4.1.11",
|
|
47
|
+
"@fluojs/di": "^3.0.0",
|
|
48
|
+
"@fluojs/testing": "^3.0.0"
|
|
48
49
|
},
|
|
49
50
|
"scripts": {
|
|
50
51
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|