@fluojs/platform-fastify 1.0.7 → 1.0.9
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 +52 -7
- package/README.md +52 -7
- package/dist/adapter.d.ts +9 -8
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +171 -35
- package/package.json +5 -5
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 >=20.0.0`을 선언합니다. 이 패키지가 HTTP 서버를 소유하는 로컬 개발, CI, 컨테이너, 프로덕션 호스트는 Node.js 20 이상에서 실행해야 합니다. 비 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 애플리케이션에 권장되는 선택입니다.
|
|
@@ -45,8 +54,41 @@ await app.listen();
|
|
|
45
54
|
|
|
46
55
|
## 주요 패턴
|
|
47
56
|
|
|
57
|
+
### HTTPS/TLS 시작
|
|
58
|
+
Fastify 프로세스가 TLS를 직접 소유할 때는 Node.js `https.ServerOptions`를 `createFastifyAdapter(...)`, `bootstrapFastifyApplication(...)`, 또는 `runFastifyApplication(...)`의 `https` option으로 전달하세요. Adapter는 Fastify를 HTTPS listener로 시작하며 startup log는 `https://host:port` URL을 보고합니다.
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
const app = await fluoFactory.create(AppModule, {
|
|
62
|
+
adapter: createFastifyAdapter({
|
|
63
|
+
host: '0.0.0.0',
|
|
64
|
+
port: 3443,
|
|
65
|
+
https: {
|
|
66
|
+
cert: tlsCertificate,
|
|
67
|
+
key: tlsPrivateKey,
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await app.listen();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Adapter를 만들기 전에 certificate는 애플리케이션 configuration 또는 secret-management boundary에서 로드하세요. 이 패키지는 certificate file, `process.env`, `PORT`를 직접 읽지 않습니다. Load balancer, ingress, API gateway가 TLS를 종료한다면 `https`를 설정하지 말고 해당 infrastructure 뒤에서 Fastify adapter를 일반 HTTP로 실행하세요.
|
|
76
|
+
|
|
77
|
+
`bootstrapFastifyApplication(...)`과 `runFastifyApplication(...)`도 같은 `https`, `host`, `port` option을 받습니다.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
await runFastifyApplication(AppModule, {
|
|
81
|
+
host: '127.0.0.1',
|
|
82
|
+
https: {
|
|
83
|
+
cert: tlsCertificate,
|
|
84
|
+
key: tlsPrivateKey,
|
|
85
|
+
},
|
|
86
|
+
port: 3443,
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
48
90
|
### 멀티파트 및 Raw Body
|
|
49
|
-
Fastify 어댑터는 내부 Fastify 플러그인을 통해 멀티파트 form-data 및 raw body 파싱을 기본적으로 지원하며, 이는 표준 fluo 요청 인터페이스를 통해 노출됩니다. `rawBody: true`를 활성화하면 멀티파트가 아닌 요청에서 `FrameworkRequest.rawBody`가 원본 요청 바이트를 그대로 보존하므로 webhook 서명 검증이나 기타 바이트 민감한 흐름에서 정확한 payload를 다시 사용할 수 있습니다. 어댑터를 직접 생성할 때는 멀티파트 제한을 두 번째 인자로 전달하고, `bootstrapFastifyApplication(...)` 및 `runFastifyApplication(...)`에서는 같은 설정을 `options.multipart` 아래에 전달하면 됩니다.
|
|
91
|
+
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
92
|
|
|
51
93
|
Multipart request에서는 `Multipart/Form-Data`처럼 대소문자가 섞인 `Content-Type` media 값도 포함해 raw-body capture를 건너뜁니다. `multipart.maxTotalSize`를 생략하면 `maxBodySize`가 기본값이 되어 HTTP adapter 간 size limit이 portable하게 유지됩니다.
|
|
52
94
|
|
|
@@ -128,7 +170,9 @@ fluo 라우트 메타데이터를 Fastify 경로로 그대로 옮길 수 있는
|
|
|
128
170
|
|
|
129
171
|
여러 라우트가 같은 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
172
|
|
|
131
|
-
어댑터는 매칭되지 않은 경로와 이식성에 민감한
|
|
173
|
+
어댑터는 매칭되지 않은 경로와 이식성에 민감한 경우, 그리고 공유 body/materialization 경로를 보존해야 하는 multipart request를 위해 와일드카드 fallback 라우트를 계속 유지하며, Fastify의 trailing slash / duplicate slash 정규화를 켜서 네이티브 선택 경로도 fluo의 문서화된 route path 계약과 맞추어 동작하도록 합니다. CORS 처리는 Fastify 플러그인이 아니라 fluo의 공유 middleware 경로가 계속 소유하고, `OPTIONS` 같은 미지원 메서드는 fluo route가 명시적으로 소유하지 않는 한 fallback dispatcher 경로로 흐릅니다.
|
|
174
|
+
|
|
175
|
+
동시에 호출된 `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
176
|
|
|
133
177
|
## 성능
|
|
134
178
|
|
|
@@ -145,13 +189,13 @@ fluo의 Fastify 어댑터는 높은 동시성 시나리오에서 raw Node.js 어
|
|
|
145
189
|
|
|
146
190
|
`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을 확인합니다.
|
|
147
191
|
|
|
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와 맞추어 유지하세요.
|
|
192
|
+
같은 파일은 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, 대소문자 구분 없는 multipart detection, multipart limit handling도 함께 다룹니다. startup, routing, adapter portability behavior를 변경할 때는 README 예제 포인터를 이 테스트 파일 및 custom adapter book chapter와 맞추어 유지하세요.
|
|
149
193
|
|
|
150
194
|
## 공개 API 개요
|
|
151
195
|
|
|
152
|
-
- `createFastifyAdapter(options)`: Fastify 어댑터를 위한 권장 팩토리입니다.
|
|
153
|
-
- `bootstrapFastifyApplication(module, options)`: 암시적 리스닝 없이 수행하는 고급 부트스트랩입니다.
|
|
154
|
-
- `runFastifyApplication(module, options)`: 생명주기 관리를 포함한 빠른 시작
|
|
196
|
+
- `createFastifyAdapter(options, multipartOptions?)`: Fastify 어댑터를 위한 권장 팩토리입니다. `options`에는 `host`, `port`, Node.js `https` server option 같은 transport startup knob이 포함됩니다. 선택적 두 번째 인자는 직접 어댑터를 생성할 때 `maxFileSize`, `maxFiles`, `maxTotalSize` 같은 multipart 제한을 설정합니다.
|
|
197
|
+
- `bootstrapFastifyApplication(module, options)`: 암시적 리스닝 없이 수행하는 고급 부트스트랩입니다. Host가 bind 전에 앱을 구성해야 할 때 `https`를 포함한 같은 Fastify startup option을 받습니다.
|
|
198
|
+
- `runFastifyApplication(module, options)`: 생명주기 관리를 포함한 빠른 시작 헬퍼이며 같은 `https` startup surface를 제공합니다. timeout/실패 시에는 해당 상태를 로그와 `process.exitCode`로 보고하고, 최종 프로세스 종료는 주변 호스트에 맡깁니다.
|
|
155
199
|
- `isFastifyMultipartTooLargeError(error)`: Fastify error shape 전반에서 multipart limit error를 감지합니다.
|
|
156
200
|
- `FastifyHttpApplicationAdapter`: 핵심 어댑터 구현 클래스입니다.
|
|
157
201
|
- Option type: `FastifyAdapterOptions`, `BootstrapFastifyApplicationOptions`, `RunFastifyApplicationOptions`, `CorsInput`, `FastifyApplicationSignal`.
|
|
@@ -163,6 +207,7 @@ fluo의 Fastify 어댑터는 높은 동시성 시나리오에서 raw Node.js 어
|
|
|
163
207
|
- **로깅 (Logging)**: 로그 스트림 중복을 방지하기 위해 Fastify의 네이티브 로거가 비활성화됩니다. `runFastifyApplication`과 `bootstrapFastifyApplication`은 framework console logger를 기본으로 선택하며, host나 test가 주입된 `ApplicationLogger`를 사용해야 할 때 `logger`를 받습니다.
|
|
164
208
|
- **글로벌 접두사 (Global Prefix)**: 내부 경로 또는 헬스 체크 엔드포인트에 접두사가 붙지 않도록 `globalPrefixExclude`를 적절히 설정하세요.
|
|
165
209
|
- **Malformed Cookie**: 잘못된 cookie header는 request 실패로 이어지지 않고 보존됩니다.
|
|
210
|
+
- **HTTPS 시작**: Fastify 프로세스가 TLS를 소유한다면 Node.js 20 이상에서 adapter `https` option 아래에 certificate material을 전달하세요. Infrastructure가 TLS를 종료한다면 해당 경계 뒤에서 adapter를 일반 HTTP로 유지하세요.
|
|
166
211
|
|
|
167
212
|
## 관련 패키지
|
|
168
213
|
|
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 >=20.0.0`. Run local development, CI, containers, and production hosts on Node.js 20 or newer when this package owns the HTTP server. 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.
|
|
@@ -45,8 +54,41 @@ await app.listen();
|
|
|
45
54
|
|
|
46
55
|
## Common Patterns
|
|
47
56
|
|
|
57
|
+
### HTTPS/TLS Startup
|
|
58
|
+
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.
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
const app = await fluoFactory.create(AppModule, {
|
|
62
|
+
adapter: createFastifyAdapter({
|
|
63
|
+
host: '0.0.0.0',
|
|
64
|
+
port: 3443,
|
|
65
|
+
https: {
|
|
66
|
+
cert: tlsCertificate,
|
|
67
|
+
key: tlsPrivateKey,
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await app.listen();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
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.
|
|
76
|
+
|
|
77
|
+
`bootstrapFastifyApplication(...)` and `runFastifyApplication(...)` accept the same `https`, `host`, and `port` options:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
await runFastifyApplication(AppModule, {
|
|
81
|
+
host: '127.0.0.1',
|
|
82
|
+
https: {
|
|
83
|
+
cert: tlsCertificate,
|
|
84
|
+
key: tlsPrivateKey,
|
|
85
|
+
},
|
|
86
|
+
port: 3443,
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
48
90
|
### 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`.
|
|
91
|
+
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
92
|
|
|
51
93
|
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
94
|
|
|
@@ -128,7 +170,9 @@ When fluo route metadata can be translated directly, the adapter registers Fasti
|
|
|
128
170
|
|
|
129
171
|
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
172
|
|
|
131
|
-
The adapter keeps a wildcard fallback route for unmatched paths and portability-sensitive cases, 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.
|
|
173
|
+
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.
|
|
174
|
+
|
|
175
|
+
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
176
|
|
|
133
177
|
## Performance
|
|
134
178
|
|
|
@@ -145,13 +189,13 @@ fluo's Fastify adapter significantly outperforms the raw Node.js adapter in high
|
|
|
145
189
|
|
|
146
190
|
`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.
|
|
147
191
|
|
|
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.
|
|
192
|
+
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, 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
193
|
|
|
150
194
|
## Public API Overview
|
|
151
195
|
|
|
152
|
-
- `createFastifyAdapter(options)`: Recommended factory for the Fastify adapter.
|
|
153
|
-
- `bootstrapFastifyApplication(module, options)`: advanced bootstrap without implicit listening.
|
|
154
|
-
- `runFastifyApplication(module, options)`: Quick-start helper with lifecycle management. On timeout/failure it reports the condition through logging and `process.exitCode`, while leaving final process termination to the surrounding host.
|
|
196
|
+
- `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.
|
|
197
|
+
- `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.
|
|
198
|
+
- `runFastifyApplication(module, options)`: Quick-start helper with lifecycle management and the same `https` startup surface. On timeout/failure it reports the condition through logging and `process.exitCode`, while leaving final process termination to the surrounding host.
|
|
155
199
|
- `isFastifyMultipartTooLargeError(error)`: Detects multipart limit errors across Fastify error shapes.
|
|
156
200
|
- `FastifyHttpApplicationAdapter`: The core adapter implementation.
|
|
157
201
|
- Option types: `FastifyAdapterOptions`, `BootstrapFastifyApplicationOptions`, `RunFastifyApplicationOptions`, `CorsInput`, `FastifyApplicationSignal`.
|
|
@@ -163,6 +207,7 @@ The same file also covers Fastify-specific native route registration with wildca
|
|
|
163
207
|
- **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
208
|
- **Global Prefix**: Use `globalPrefixExclude` to prevent the prefix from being applied to internal routes or health check endpoints.
|
|
165
209
|
- **Malformed Cookies**: Malformed cookie headers are preserved rather than failing the request.
|
|
210
|
+
- **HTTPS startup**: Use Node.js 20+ 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.
|
|
166
211
|
|
|
167
212
|
## Related Packages
|
|
168
213
|
|
package/dist/adapter.d.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
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
|
-
declare module '@fluojs/http' {
|
|
5
|
-
interface FrameworkRequest {
|
|
6
|
-
files?: UploadedFile[];
|
|
7
|
-
rawBody?: Uint8Array;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
3
|
+
import type { Application, ApplicationLogger, CreateApplicationOptions, ModuleType, MultipartOptions } from '@fluojs/runtime';
|
|
10
4
|
/**
|
|
11
5
|
* Transport-level knobs for the standalone Fastify HTTP adapter factory.
|
|
12
6
|
*/
|
|
@@ -74,8 +68,13 @@ export declare class FastifyHttpApplicationAdapter implements HttpApplicationAda
|
|
|
74
68
|
private readonly shutdownTimeoutMs;
|
|
75
69
|
private closeInFlight?;
|
|
76
70
|
private dispatcher?;
|
|
71
|
+
private appClosed;
|
|
72
|
+
private listenAbortController?;
|
|
73
|
+
private listenInFlight?;
|
|
74
|
+
private listenState;
|
|
75
|
+
private readonly nativeRouteDescriptors;
|
|
77
76
|
private pluginsReady;
|
|
78
|
-
private
|
|
77
|
+
private app;
|
|
79
78
|
private readonly requestResponseFactory;
|
|
80
79
|
constructor(port: number, host: string | undefined, retryDelayMs: number | undefined, retryLimit: number | undefined, httpsOptions: HttpsServerOptions | undefined, multipartOptions?: MultipartOptions | undefined, maxBodySize?: number, preserveRawBody?: boolean, shutdownTimeoutMs?: number);
|
|
81
80
|
getServer(): unknown;
|
|
@@ -83,7 +82,9 @@ export declare class FastifyHttpApplicationAdapter implements HttpApplicationAda
|
|
|
83
82
|
getListenTarget(): FastifyListenTarget;
|
|
84
83
|
listen(dispatcher: Dispatcher): Promise<void>;
|
|
85
84
|
close(): Promise<void>;
|
|
85
|
+
private closeApplication;
|
|
86
86
|
private registerPluginsAndRoutes;
|
|
87
|
+
private configureNativeRouteDescriptors;
|
|
87
88
|
private registerNativeRoutes;
|
|
88
89
|
private registerWildcardFallbackRoute;
|
|
89
90
|
private listenWithRetry;
|
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
|
@@ -39,6 +39,11 @@ const EMPTY_NATIVE_ROUTE_PARAMS = Object.freeze({});
|
|
|
39
39
|
export class FastifyHttpApplicationAdapter {
|
|
40
40
|
closeInFlight;
|
|
41
41
|
dispatcher;
|
|
42
|
+
appClosed = false;
|
|
43
|
+
listenAbortController;
|
|
44
|
+
listenInFlight;
|
|
45
|
+
listenState = 'idle';
|
|
46
|
+
nativeRouteDescriptors = new Map();
|
|
42
47
|
pluginsReady = false;
|
|
43
48
|
app;
|
|
44
49
|
requestResponseFactory;
|
|
@@ -69,28 +74,70 @@ export class FastifyHttpApplicationAdapter {
|
|
|
69
74
|
getListenTarget() {
|
|
70
75
|
return resolveListenTarget(this.app.server.address() ?? null, this.port, this.host, this.httpsOptions !== undefined);
|
|
71
76
|
}
|
|
72
|
-
|
|
77
|
+
listen(dispatcher) {
|
|
78
|
+
if (this.closeInFlight) {
|
|
79
|
+
return this.closeInFlight.then(() => this.listen(dispatcher));
|
|
80
|
+
}
|
|
81
|
+
if (this.listenState === 'listening') {
|
|
82
|
+
return Promise.resolve();
|
|
83
|
+
}
|
|
84
|
+
if (this.listenInFlight) {
|
|
85
|
+
return this.listenInFlight;
|
|
86
|
+
}
|
|
87
|
+
if (this.appClosed) {
|
|
88
|
+
this.app = createFastifyApp(this.httpsOptions, this.maxBodySize);
|
|
89
|
+
this.appClosed = false;
|
|
90
|
+
this.pluginsReady = false;
|
|
91
|
+
}
|
|
73
92
|
this.dispatcher = dispatcher;
|
|
74
|
-
|
|
75
|
-
|
|
93
|
+
this.configureNativeRouteDescriptors(dispatcher);
|
|
94
|
+
const abortController = new AbortController();
|
|
95
|
+
this.listenAbortController = abortController;
|
|
96
|
+
this.listenState = 'starting';
|
|
97
|
+
const listenInFlight = this.registerPluginsAndRoutes(dispatcher).then(() => this.listenWithRetry(abortController.signal)).then(() => {
|
|
98
|
+
this.listenState = 'listening';
|
|
99
|
+
}, error => {
|
|
100
|
+
this.listenState = 'idle';
|
|
101
|
+
throw error;
|
|
102
|
+
}).finally(() => {
|
|
103
|
+
if (this.listenInFlight === listenInFlight) {
|
|
104
|
+
this.listenInFlight = undefined;
|
|
105
|
+
}
|
|
106
|
+
if (this.listenAbortController === abortController) {
|
|
107
|
+
this.listenAbortController = undefined;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
this.listenInFlight = listenInFlight;
|
|
111
|
+
return listenInFlight;
|
|
76
112
|
}
|
|
77
|
-
|
|
78
|
-
if (this.closeInFlight) {
|
|
79
|
-
|
|
80
|
-
|
|
113
|
+
close() {
|
|
114
|
+
if (!this.closeInFlight) {
|
|
115
|
+
const closeInFlight = this.closeApplication().finally(() => {
|
|
116
|
+
if (this.closeInFlight === closeInFlight) {
|
|
117
|
+
this.closeInFlight = undefined;
|
|
118
|
+
}
|
|
119
|
+
this.listenState = 'idle';
|
|
120
|
+
this.dispatcher = undefined;
|
|
121
|
+
this.nativeRouteDescriptors.clear();
|
|
122
|
+
});
|
|
123
|
+
this.closeInFlight = closeInFlight;
|
|
124
|
+
void closeInFlight.catch(() => {});
|
|
125
|
+
}
|
|
126
|
+
return waitForCloseWithTimeout(this.closeInFlight, this.shutdownTimeoutMs);
|
|
127
|
+
}
|
|
128
|
+
async closeApplication() {
|
|
129
|
+
if (this.listenInFlight) {
|
|
130
|
+
this.listenAbortController?.abort();
|
|
131
|
+
await ignoreCancelledListen(this.listenInFlight);
|
|
81
132
|
}
|
|
82
133
|
if (!this.app.server.listening) {
|
|
83
|
-
this.dispatcher = undefined;
|
|
84
134
|
return;
|
|
85
135
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
this.
|
|
90
|
-
}
|
|
91
|
-
this.closeInFlight = closeInFlight;
|
|
92
|
-
void closeInFlight.catch(() => {});
|
|
93
|
-
await waitForCloseWithTimeout(closeInFlight, this.shutdownTimeoutMs);
|
|
136
|
+
try {
|
|
137
|
+
await this.app.close();
|
|
138
|
+
} finally {
|
|
139
|
+
this.appClosed = true;
|
|
140
|
+
}
|
|
94
141
|
}
|
|
95
142
|
async registerPluginsAndRoutes(dispatcher) {
|
|
96
143
|
if (this.pluginsReady) {
|
|
@@ -104,14 +151,21 @@ export class FastifyHttpApplicationAdapter {
|
|
|
104
151
|
this.registerWildcardFallbackRoute();
|
|
105
152
|
this.pluginsReady = true;
|
|
106
153
|
}
|
|
154
|
+
configureNativeRouteDescriptors(dispatcher) {
|
|
155
|
+
this.nativeRouteDescriptors.clear();
|
|
156
|
+
for (const route of createFastifyNativeRoutes(resolveDispatcherRouteDescriptors(dispatcher))) {
|
|
157
|
+
this.nativeRouteDescriptors.set(route.routeKey, route.descriptor);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
107
160
|
registerNativeRoutes(descriptors) {
|
|
108
161
|
for (const route of createFastifyNativeRoutes(descriptors)) {
|
|
109
162
|
this.app.route({
|
|
110
163
|
handler: async (request, reply) => {
|
|
111
164
|
const urlParts = splitRawRequestUrl(request.raw.url ?? '/');
|
|
112
165
|
const params = normalizeNativeRouteParams(request.params);
|
|
113
|
-
|
|
114
|
-
|
|
166
|
+
const descriptor = this.nativeRouteDescriptors.get(route.routeKey);
|
|
167
|
+
if (descriptor && !isRoutePathNormalizationSensitive(urlParts.path) && !hasNativeRouteParamSeparators(params)) {
|
|
168
|
+
await this.handleNativeRouteRequest(descriptor, params, urlParts, request, reply);
|
|
115
169
|
return;
|
|
116
170
|
}
|
|
117
171
|
await this.handleRequest(request, reply);
|
|
@@ -126,19 +180,21 @@ export class FastifyHttpApplicationAdapter {
|
|
|
126
180
|
await this.handleRequest(request, reply);
|
|
127
181
|
});
|
|
128
182
|
}
|
|
129
|
-
async listenWithRetry() {
|
|
183
|
+
async listenWithRetry(signal) {
|
|
130
184
|
for (let attempt = 0;; attempt++) {
|
|
185
|
+
throwIfListenCancelled(signal);
|
|
131
186
|
try {
|
|
132
187
|
await this.app.listen({
|
|
133
188
|
host: this.host,
|
|
134
189
|
port: this.port
|
|
135
190
|
});
|
|
191
|
+
throwIfListenCancelled(signal);
|
|
136
192
|
return;
|
|
137
193
|
} catch (error) {
|
|
138
194
|
if (!isAddressInUseError(error) || attempt >= this.retryLimit) {
|
|
139
195
|
throw error;
|
|
140
196
|
}
|
|
141
|
-
await delay(this.retryDelayMs);
|
|
197
|
+
await delay(this.retryDelayMs, signal);
|
|
142
198
|
}
|
|
143
199
|
}
|
|
144
200
|
}
|
|
@@ -209,6 +265,7 @@ function createNativeFastFrameworkRequest(request, lazySignal, urlParts, maxBody
|
|
|
209
265
|
if (Number.isFinite(contentLength) && contentLength > maxBodySize) {
|
|
210
266
|
throw new PayloadTooLargeException('Request body exceeds the size limit.');
|
|
211
267
|
}
|
|
268
|
+
assertBodyWithinMaxBodySize(request.body, maxBodySize);
|
|
212
269
|
const frameworkRequest = createDeferredFrameworkRequestShell({
|
|
213
270
|
cookieHeader: cloneHeaderValue(request.headers.cookie),
|
|
214
271
|
headersFactory: () => normalizeHeaders(cloneRequestHeaders(request.headers)),
|
|
@@ -217,7 +274,7 @@ function createNativeFastFrameworkRequest(request, lazySignal, urlParts, maxBody
|
|
|
217
274
|
query: readSimpleQueryRecord(request.query),
|
|
218
275
|
queryFactory: () => parseQueryParamsFromSearch(urlParts.search),
|
|
219
276
|
raw: request.raw,
|
|
220
|
-
requestId:
|
|
277
|
+
requestId: resolveRequestIdFromHeaders(request.raw.headers),
|
|
221
278
|
signal: lazySignal.signal,
|
|
222
279
|
url: urlParts.path + urlParts.search
|
|
223
280
|
});
|
|
@@ -314,11 +371,13 @@ function createFastifyNativeRoutes(descriptors) {
|
|
|
314
371
|
return [...candidates.values()].filter(candidate => shapePaths.get(candidate.shapeKey)?.size === 1).map(({
|
|
315
372
|
descriptor,
|
|
316
373
|
method,
|
|
317
|
-
path
|
|
374
|
+
path,
|
|
375
|
+
routeKey
|
|
318
376
|
}) => ({
|
|
319
377
|
descriptor,
|
|
320
378
|
method,
|
|
321
|
-
path
|
|
379
|
+
path,
|
|
380
|
+
routeKey
|
|
322
381
|
}));
|
|
323
382
|
}
|
|
324
383
|
function isFastifyNativeRouteDescriptor(descriptor) {
|
|
@@ -334,6 +393,7 @@ function registerFastifyNativeRouteCandidate(candidates, shapePaths, descriptor)
|
|
|
334
393
|
descriptor,
|
|
335
394
|
method,
|
|
336
395
|
path,
|
|
396
|
+
routeKey,
|
|
337
397
|
shapeKey
|
|
338
398
|
});
|
|
339
399
|
}
|
|
@@ -550,6 +610,7 @@ function createDeferredFrameworkRequest(request, signal, multipartOptions, maxBo
|
|
|
550
610
|
if (preserveRawBody && !isMultipart) {
|
|
551
611
|
const rawBodyValue = request.rawBody;
|
|
552
612
|
if (rawBodyValue !== undefined) {
|
|
613
|
+
assertBodyWithinMaxBodySize(rawBodyValue, maxBodySize);
|
|
553
614
|
frameworkRequest.rawBody = rawBodyValue;
|
|
554
615
|
}
|
|
555
616
|
}
|
|
@@ -563,11 +624,12 @@ function createDeferredFrameworkRequest(request, signal, multipartOptions, maxBo
|
|
|
563
624
|
query: querySnapshot,
|
|
564
625
|
queryFactory: () => parseQueryParamsFromSearch(urlParts.search),
|
|
565
626
|
raw: request.raw,
|
|
566
|
-
requestId:
|
|
627
|
+
requestId: resolveRequestIdFromHeaders(headerSnapshot),
|
|
567
628
|
signal,
|
|
568
629
|
url: urlParts.path + urlParts.search
|
|
569
630
|
});
|
|
570
631
|
if (!needsDeferredBodyMaterialization) {
|
|
632
|
+
assertBodyWithinMaxBodySize(request.body, maxBodySize);
|
|
571
633
|
frameworkRequest.body = request.body;
|
|
572
634
|
}
|
|
573
635
|
const nativeRouteHandoff = consumeRawRequestNativeRouteHandoff(request.raw);
|
|
@@ -603,6 +665,26 @@ function hasNativeRouteParamSeparators(params) {
|
|
|
603
665
|
}
|
|
604
666
|
return false;
|
|
605
667
|
}
|
|
668
|
+
function assertBodyWithinMaxBodySize(body, maxBodySize) {
|
|
669
|
+
if (resolveFastifyBodySize(body) > maxBodySize) {
|
|
670
|
+
throw new PayloadTooLargeException('Request body exceeds the size limit.');
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function resolveFastifyBodySize(body) {
|
|
674
|
+
if (body === undefined || body === null) {
|
|
675
|
+
return 0;
|
|
676
|
+
}
|
|
677
|
+
if (typeof body === 'string') {
|
|
678
|
+
return Buffer.byteLength(body, 'utf8');
|
|
679
|
+
}
|
|
680
|
+
if (Buffer.isBuffer(body)) {
|
|
681
|
+
return body.byteLength;
|
|
682
|
+
}
|
|
683
|
+
if (body instanceof Uint8Array) {
|
|
684
|
+
return body.byteLength;
|
|
685
|
+
}
|
|
686
|
+
return 1;
|
|
687
|
+
}
|
|
606
688
|
function collectVersionSensitiveRouteKeys(descriptors) {
|
|
607
689
|
const grouped = new Map();
|
|
608
690
|
for (const descriptor of descriptors) {
|
|
@@ -746,14 +828,10 @@ function resolveRequestIdFromHeaders(headers) {
|
|
|
746
828
|
const requestId = headers['x-request-id'] ?? headers['x-correlation-id'];
|
|
747
829
|
return Array.isArray(requestId) ? requestId[0] : requestId;
|
|
748
830
|
}
|
|
749
|
-
function resolvePrimaryRequestIdFromHeaders(headers) {
|
|
750
|
-
const requestId = headers['x-request-id'];
|
|
751
|
-
return Array.isArray(requestId) ? requestId[0] : requestId;
|
|
752
|
-
}
|
|
753
831
|
function createFastifyApp(httpsOptions, maxBodySize) {
|
|
754
832
|
if (httpsOptions) {
|
|
755
833
|
return fastify({
|
|
756
|
-
bodyLimit: maxBodySize,
|
|
834
|
+
bodyLimit: resolveFastifyBodyLimit(maxBodySize),
|
|
757
835
|
exposeHeadRoutes: false,
|
|
758
836
|
https: httpsOptions,
|
|
759
837
|
logger: false,
|
|
@@ -764,7 +842,7 @@ function createFastifyApp(httpsOptions, maxBodySize) {
|
|
|
764
842
|
});
|
|
765
843
|
}
|
|
766
844
|
return fastify({
|
|
767
|
-
bodyLimit: maxBodySize,
|
|
845
|
+
bodyLimit: resolveFastifyBodyLimit(maxBodySize),
|
|
768
846
|
exposeHeadRoutes: false,
|
|
769
847
|
logger: false,
|
|
770
848
|
routerOptions: {
|
|
@@ -773,6 +851,9 @@ function createFastifyApp(httpsOptions, maxBodySize) {
|
|
|
773
851
|
}
|
|
774
852
|
});
|
|
775
853
|
}
|
|
854
|
+
function resolveFastifyBodyLimit(maxBodySize) {
|
|
855
|
+
return Math.max(maxBodySize, 1);
|
|
856
|
+
}
|
|
776
857
|
function captureRawBodyPreParsingHook(request, _reply, payload, done) {
|
|
777
858
|
if (isMultipartRequestContentType(request.headers['content-type'])) {
|
|
778
859
|
done(null, payload);
|
|
@@ -780,8 +861,14 @@ function captureRawBodyPreParsingHook(request, _reply, payload, done) {
|
|
|
780
861
|
}
|
|
781
862
|
const chunks = [];
|
|
782
863
|
const capture = new Transform({
|
|
783
|
-
transform(chunk,
|
|
784
|
-
|
|
864
|
+
transform(chunk, encoding, callback) {
|
|
865
|
+
let bufferChunk;
|
|
866
|
+
try {
|
|
867
|
+
bufferChunk = createRawBodyBufferChunk(chunk, encoding);
|
|
868
|
+
} catch (error) {
|
|
869
|
+
callback(error instanceof Error ? error : new Error(String(error)));
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
785
872
|
chunks.push(bufferChunk);
|
|
786
873
|
capture.receivedEncodedLength += bufferChunk.byteLength;
|
|
787
874
|
callback(null, chunk);
|
|
@@ -800,6 +887,18 @@ function captureRawBodyPreParsingHook(request, _reply, payload, done) {
|
|
|
800
887
|
payload.pipe(capture);
|
|
801
888
|
done(null, capture);
|
|
802
889
|
}
|
|
890
|
+
function createRawBodyBufferChunk(chunk, encoding) {
|
|
891
|
+
if (Buffer.isBuffer(chunk)) {
|
|
892
|
+
return chunk;
|
|
893
|
+
}
|
|
894
|
+
if (chunk instanceof Uint8Array) {
|
|
895
|
+
return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
896
|
+
}
|
|
897
|
+
if (typeof chunk === 'string') {
|
|
898
|
+
return Buffer.from(chunk, encoding);
|
|
899
|
+
}
|
|
900
|
+
throw new TypeError(`Fastify raw-body capture received unsupported ${typeof chunk} stream chunk.`);
|
|
901
|
+
}
|
|
803
902
|
function isMultipartRequestContentType(contentType) {
|
|
804
903
|
const primaryValue = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
805
904
|
return typeof primaryValue === 'string' && primaryValue.toLowerCase().includes('multipart/form-data');
|
|
@@ -853,9 +952,46 @@ function isAddressInUseError(error) {
|
|
|
853
952
|
}
|
|
854
953
|
return error.code === 'EADDRINUSE';
|
|
855
954
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
955
|
+
class FastifyListenCancelledError extends Error {
|
|
956
|
+
constructor() {
|
|
957
|
+
super('Fastify adapter startup was cancelled during shutdown.');
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function throwIfListenCancelled(signal) {
|
|
961
|
+
if (signal.aborted) {
|
|
962
|
+
throw new FastifyListenCancelledError();
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
function isFastifyListenCancelledError(error) {
|
|
966
|
+
return error instanceof FastifyListenCancelledError;
|
|
967
|
+
}
|
|
968
|
+
async function ignoreCancelledListen(listenPromise) {
|
|
969
|
+
try {
|
|
970
|
+
await listenPromise;
|
|
971
|
+
} catch (error) {
|
|
972
|
+
if (isFastifyListenCancelledError(error)) {
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
throw error;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function delay(ms, signal) {
|
|
979
|
+
return new Promise((resolve, reject) => {
|
|
980
|
+
if (signal?.aborted) {
|
|
981
|
+
reject(new FastifyListenCancelledError());
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
const timeout = setTimeout(() => {
|
|
985
|
+
signal?.removeEventListener('abort', onAbort);
|
|
986
|
+
resolve();
|
|
987
|
+
}, ms);
|
|
988
|
+
const onAbort = () => {
|
|
989
|
+
clearTimeout(timeout);
|
|
990
|
+
reject(new FastifyListenCancelledError());
|
|
991
|
+
};
|
|
992
|
+
signal?.addEventListener('abort', onAbort, {
|
|
993
|
+
once: true
|
|
994
|
+
});
|
|
859
995
|
});
|
|
860
996
|
}
|
|
861
997
|
function waitForCloseWithTimeout(closePromise, timeoutMs) {
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"platform",
|
|
9
9
|
"server"
|
|
10
10
|
],
|
|
11
|
-
"version": "1.0.
|
|
11
|
+
"version": "1.0.9",
|
|
12
12
|
"private": false,
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"repository": {
|
|
@@ -38,13 +38,13 @@
|
|
|
38
38
|
"@fastify/multipart": "^9.2.1",
|
|
39
39
|
"fastify": "^5.8.5",
|
|
40
40
|
"fastify-raw-body": "^5.0.0",
|
|
41
|
-
"@fluojs/http": "^
|
|
42
|
-
"@fluojs/runtime": "^
|
|
41
|
+
"@fluojs/http": "^2.0.1",
|
|
42
|
+
"@fluojs/runtime": "^2.0.1"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"vitest": "^3.2.4",
|
|
46
|
-
"@fluojs/di": "^
|
|
47
|
-
"@fluojs/testing": "^
|
|
46
|
+
"@fluojs/di": "^2.0.0",
|
|
47
|
+
"@fluojs/testing": "^2.0.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|