@fluojs/http 1.1.2 → 3.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 +430 -15
- package/README.md +437 -15
- package/dist/access-log-observer.d.ts +77 -0
- package/dist/access-log-observer.d.ts.map +1 -0
- package/dist/access-log-observer.js +171 -0
- package/dist/adapter.d.ts +9 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +15 -1
- package/dist/adapters/binding.d.ts.map +1 -1
- package/dist/adapters/binding.js +18 -16
- package/dist/adapters/dto-binding-plan.d.ts +2 -2
- package/dist/adapters/dto-binding-plan.d.ts.map +1 -1
- package/dist/adapters/dto-binding-plan.js +13 -4
- package/dist/adapters/dto-validation-adapter.d.ts +2 -1
- package/dist/adapters/dto-validation-adapter.d.ts.map +1 -1
- package/dist/adapters/dto-validation-adapter.js +2 -2
- package/dist/byte-range-response.d.ts +71 -0
- package/dist/byte-range-response.d.ts.map +1 -0
- package/dist/byte-range-response.js +347 -0
- package/dist/client-identity.d.ts +8 -4
- package/dist/client-identity.d.ts.map +1 -1
- package/dist/client-identity.js +12 -93
- package/dist/compiled-route-identity.d.ts +17 -0
- package/dist/compiled-route-identity.d.ts.map +1 -0
- package/dist/compiled-route-identity.js +29 -0
- package/dist/connection.d.ts +67 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +480 -0
- package/dist/context/request-context-node-store.d.ts +6 -0
- package/dist/context/request-context-node-store.d.ts.map +1 -1
- package/dist/context/request-context-node-store.js +14 -0
- package/dist/context/request-context.d.ts +5 -5
- package/dist/context/request-context.d.ts.map +1 -1
- package/dist/context/request-context.js +8 -83
- package/dist/context/request-id.d.ts +10 -0
- package/dist/context/request-id.d.ts.map +1 -0
- package/dist/context/request-id.js +27 -0
- package/dist/context/sse.d.ts +13 -0
- package/dist/context/sse.d.ts.map +1 -1
- package/dist/context/sse.js +36 -6
- package/dist/cookie-helpers.d.ts +47 -0
- package/dist/cookie-helpers.d.ts.map +1 -0
- package/dist/cookie-helpers.js +166 -0
- package/dist/decorators.d.ts +52 -19
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +57 -10
- package/dist/dispatch/conditional-request-policy.d.ts +67 -0
- package/dist/dispatch/conditional-request-policy.d.ts.map +1 -0
- package/dist/dispatch/conditional-request-policy.js +297 -0
- package/dist/dispatch/dispatch-content-negotiation.d.ts +8 -0
- package/dist/dispatch/dispatch-content-negotiation.d.ts.map +1 -1
- package/dist/dispatch/dispatch-content-negotiation.js +89 -42
- package/dist/dispatch/dispatch-error-negotiation.d.ts +26 -0
- package/dist/dispatch/dispatch-error-negotiation.d.ts.map +1 -0
- package/dist/dispatch/dispatch-error-negotiation.js +110 -0
- package/dist/dispatch/dispatch-error-policy.d.ts +1 -10
- package/dist/dispatch/dispatch-error-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-error-policy.js +1 -33
- package/dist/dispatch/dispatch-error-representation.d.ts +18 -0
- package/dist/dispatch/dispatch-error-representation.d.ts.map +1 -0
- package/dist/dispatch/dispatch-error-representation.js +125 -0
- package/dist/dispatch/dispatch-response-metadata.d.ts +10 -0
- package/dist/dispatch/dispatch-response-metadata.d.ts.map +1 -0
- package/dist/dispatch/dispatch-response-metadata.js +12 -0
- package/dist/dispatch/dispatch-response-policy.d.ts +33 -5
- package/dist/dispatch/dispatch-response-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-response-policy.js +149 -18
- package/dist/dispatch/dispatcher.d.ts +5 -1
- package/dist/dispatch/dispatcher.d.ts.map +1 -1
- package/dist/dispatch/dispatcher.js +178 -107
- package/dist/dispatch/fast-path/debug-visibility.d.ts.map +1 -1
- package/dist/dispatch/fast-path/debug-visibility.js +6 -3
- package/dist/dispatch/fast-path/dispatcher-state.d.ts +10 -0
- package/dist/dispatch/fast-path/dispatcher-state.d.ts.map +1 -0
- package/dist/dispatch/fast-path/dispatcher-state.js +52 -0
- package/dist/dispatch/fast-path/eligibility-checker.d.ts.map +1 -1
- package/dist/dispatch/fast-path/eligibility-checker.js +27 -15
- package/dist/dispatch/fast-path/eligibility.d.ts +16 -10
- package/dist/dispatch/fast-path/eligibility.d.ts.map +1 -1
- package/dist/dispatch/fast-path/eligibility.js +10 -4
- package/dist/dispatch/fast-path/fast-path-executor.d.ts +14 -1
- package/dist/dispatch/fast-path/fast-path-executor.d.ts.map +1 -1
- package/dist/dispatch/fast-path/fast-path-executor.js +23 -6
- package/dist/dispatch/request-abort.d.ts +22 -0
- package/dist/dispatch/request-abort.d.ts.map +1 -0
- package/dist/dispatch/request-abort.js +30 -0
- package/dist/dispatch/response-integration.d.ts +55 -0
- package/dist/dispatch/response-integration.d.ts.map +1 -0
- package/dist/dispatch/response-integration.js +57 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +19 -0
- package/dist/guards.d.ts +1 -1
- package/dist/guards.d.ts.map +1 -1
- package/dist/header-helpers.d.ts +59 -0
- package/dist/header-helpers.d.ts.map +1 -0
- package/dist/header-helpers.js +185 -0
- package/dist/index.d.ts +1 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -15
- package/dist/index.portable.d.ts +25 -0
- package/dist/index.portable.d.ts.map +1 -0
- package/dist/index.portable.js +21 -0
- package/dist/internal.d.ts +3 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -0
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +100 -15
- package/dist/middleware/correlation.d.ts +9 -1
- package/dist/middleware/correlation.d.ts.map +1 -1
- package/dist/middleware/correlation.js +25 -14
- package/dist/middleware/cors.d.ts.map +1 -1
- package/dist/middleware/cors.js +7 -8
- package/dist/middleware/middleware.d.ts +3 -3
- package/dist/middleware/middleware.d.ts.map +1 -1
- package/dist/middleware/rate-limit.d.ts +13 -2
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/middleware/rate-limit.js +1 -0
- package/dist/static-assets.d.ts +77 -0
- package/dist/static-assets.d.ts.map +1 -0
- package/dist/static-assets.js +270 -0
- package/dist/types.d.ts +225 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +71 -1
- package/package.json +19 -9
package/README.ko.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
<p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
|
|
4
4
|
|
|
5
|
+
Node.js 지원 범위는 `>=24.0.0 <27`입니다. 업그레이드 절차는 [Node.js 지원 및 마이그레이션](../../docs/reference/node-support.ko.md)을 참조하세요.
|
|
6
|
+
|
|
5
7
|
라우트 메타데이터를 DTO 바인딩, 검증, 가드, 인터셉터, 응답 작성으로 이어지는 요청 파이프라인으로 바꾸는 HTTP 실행 레이어입니다.
|
|
6
8
|
|
|
7
9
|
## 목차
|
|
@@ -10,6 +12,12 @@
|
|
|
10
12
|
- [사용 시점](#사용-시점)
|
|
11
13
|
- [빠른 시작](#빠른-시작)
|
|
12
14
|
- [주요 패턴](#주요-패턴)
|
|
15
|
+
- [정적 에셋 제공](#정적-에셋-제공)
|
|
16
|
+
- [응답 쿠키](#응답-쿠키)
|
|
17
|
+
- [Early Hints](#early-hints)
|
|
18
|
+
- [Realtime Adapter Capabilities](#realtime-adapter-capabilities)
|
|
19
|
+
- [바이트 범위 응답](#바이트-범위-응답)
|
|
20
|
+
- [HTTP Error Representations](#http-error-representations)
|
|
13
21
|
- [요청 정리와 런타임 이식성](#요청-정리와-런타임-이식성)
|
|
14
22
|
- [공개 API](#공개-api)
|
|
15
23
|
- [관련 패키지](#관련-패키지)
|
|
@@ -21,6 +29,29 @@
|
|
|
21
29
|
npm install @fluojs/http
|
|
22
30
|
```
|
|
23
31
|
|
|
32
|
+
## 정적 에셋 제공
|
|
33
|
+
|
|
34
|
+
`createStaticAssetsMiddleware(...)`는 애플리케이션이 소유한 명시적 `StaticAssetSource`를 통해 `GET` 및 `HEAD` 요청만 제공합니다. portable HTTP 패키지는 filesystem 접근을 가정하지 않으므로 Web 및 edge 애플리케이션은 암묵적 Node fallback 대신 자체 source를 제공해야 합니다.
|
|
35
|
+
|
|
36
|
+
미들웨어는 각 URL segment를 한 번만 decode하고 source 해석 전에 traversal, encoded separator, backslash, NUL을 거부합니다. Dotfile은 명시적 policy를 사용합니다. `allow`는 해석하고, `ignore`는 이후 middleware/route에 요청을 넘기며, `deny`는 설정한 dotfile index를 포함해 `403`을 commit합니다. Directory index는 기본적으로 비활성화되며 trailing slash URL에서만 고려됩니다.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { createStaticAssetsMiddleware } from '@fluojs/http';
|
|
40
|
+
import { createNodeFileSystemAssetSource } from '@fluojs/platform-nodejs';
|
|
41
|
+
|
|
42
|
+
const assets = createStaticAssetsMiddleware({
|
|
43
|
+
cacheControl: 'public, max-age=3600',
|
|
44
|
+
index: ['index.html'],
|
|
45
|
+
prefix: '/assets',
|
|
46
|
+
source: createNodeFileSystemAssetSource({
|
|
47
|
+
precompressed: true,
|
|
48
|
+
root: './public',
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Runtime bootstrap의 `middleware`에 `assets`를 등록하세요. 선택된 representation은 MIME type, 정확한 byte와 length, `ETag`, `Last-Modified`, 선택적 `Content-Encoding`을 소유합니다. static write는 adapter의 dynamic compression을 우회하므로 full `GET`, `HEAD`, conditional field, `Range`, `If-Range`에서도 이 값이 일관됩니다. Source는 request가 허용한 `br`, `gzip`, identity byte만 선택하고, 허용되는 representation이 없으면 bodyless `406`을 명시적으로 반환하며, 선택이 달라질 수 있을 때 `Vary: Accept-Encoding`을 사용합니다. Byte range는 선택된 encoded representation을 대상으로 합니다.
|
|
54
|
+
|
|
24
55
|
## 사용 시점
|
|
25
56
|
|
|
26
57
|
- `@Controller`, `@Get`, `@Post` 같은 데코레이터로 REST 스타일 엔드포인트를 선언할 때
|
|
@@ -38,12 +69,12 @@ class CreateUserDto {
|
|
|
38
69
|
@FromBody()
|
|
39
70
|
@IsString()
|
|
40
71
|
@MinLength(3)
|
|
41
|
-
name
|
|
72
|
+
name = '';
|
|
42
73
|
}
|
|
43
74
|
|
|
44
75
|
class FindUserParamsDto {
|
|
45
76
|
@FromPath('id')
|
|
46
|
-
id
|
|
77
|
+
id = '';
|
|
47
78
|
}
|
|
48
79
|
|
|
49
80
|
@Controller('/users')
|
|
@@ -62,6 +93,23 @@ export class UserController {
|
|
|
62
93
|
}
|
|
63
94
|
```
|
|
64
95
|
|
|
96
|
+
데코레이터가 붙은 DTO 필드는 위 예시처럼 초기화하거나 optional로 선언하세요. `name!: string` 같은 definite assignment assertion은 Fluo가 제공하는 Babel decorator 설정에서 컴파일되지 않습니다. 해당 설정은 데코레이터가 붙은 클래스의 definitely assigned field를 `Definitely assigned fields cannot be initialized here, but only in the constructor` 오류로 거부합니다.
|
|
97
|
+
|
|
98
|
+
### 선택적 route path
|
|
99
|
+
|
|
100
|
+
`Get`, `Post`, `Put`, `Patch`, `Delete`, `Options`, `Head`, `All`, `Sse`, HTTP `Query`는
|
|
101
|
+
path 생략 또는 `undefined`를 `''`로 받습니다. `Route(method)`도 path 기본값은 같지만
|
|
102
|
+
method는 필수입니다. `@Controller('cats')`의 `@Get()`은 `GET /cats`, `@Controller()`
|
|
103
|
+
아래에서는 `GET /`입니다. `('/')`는 유효 route가 같지만 raw path가 다르며 controller
|
|
104
|
+
prefix를 무시하지 않습니다. 중복 검출과 잘못된 path/method 거부는 유지합니다.
|
|
105
|
+
무인수 factory이지 bare `@Get` decorator가 아닙니다.
|
|
106
|
+
|
|
107
|
+
`Sse()`는 GET + `text/event-stream`과 stream lifecycle을 유지합니다. `All()`은 wildcard,
|
|
108
|
+
HTTP `Query()`는 query-string binding이나 GraphQL Query가 아닌 RFC QUERY입니다. Root와
|
|
109
|
+
`@fluojs/http/portable`은 같은 기본값을 제공하지만 adapter method 지원을 확장하지 않습니다.
|
|
110
|
+
`HttpCode(status)`, `Header(name, value)` 같은 필수 인수는
|
|
111
|
+
[165개 API 전체 대조표](../../docs/reference/decorator-defaults.ko.md)를 참고하세요.
|
|
112
|
+
|
|
65
113
|
### 라우트 경로 계약
|
|
66
114
|
|
|
67
115
|
`@Controller()`, `@Get()`, `@Post()` 같은 HTTP 라우트 데코레이터는 다음만 허용합니다.
|
|
@@ -73,6 +121,123 @@ export class UserController {
|
|
|
73
121
|
|
|
74
122
|
라우트 데코레이터는 `*`, `?`, `/(.*)`, `user-:id`, `:id.json` 같은 wildcard, regex 유사 문법, mixed segment를 지원하지 않습니다. 와일드카드 매칭은 계속 `forRoutes('/users/*')` 같은 미들웨어 설정에서만 지원됩니다.
|
|
75
123
|
|
|
124
|
+
Catch-all route grammar는 의도적으로 도입이 유예된 상태입니다.
|
|
125
|
+
[HTTP catch-all route grammar 결정](../../docs/architecture/http-catch-all-route-grammar.ko.md)은 검토한
|
|
126
|
+
syntax, provisional precedence 및 params shape, OpenAPI 제한, adapter native fast-path 제약, 이 HTTP
|
|
127
|
+
contract를 재검토하기 전에 필요한 evidence를 기록합니다. 해당 문서의 syntax는 현재 활성 route behavior가
|
|
128
|
+
아닙니다.
|
|
129
|
+
|
|
130
|
+
### Custom HTTP method 계약
|
|
131
|
+
|
|
132
|
+
RFC `QUERY`에는 `@Query(path)`를 사용하고, `PURGE` 또는 WebDAV `PROPFIND` 같은 다른 HTTP extension method에는 `@Route(method, path)`를 사용합니다.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { Controller, Query, Route } from '@fluojs/http';
|
|
136
|
+
|
|
137
|
+
@Controller('/operations')
|
|
138
|
+
export class OperationsController {
|
|
139
|
+
@Query('/search')
|
|
140
|
+
search() {
|
|
141
|
+
return { method: 'QUERY' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@Route('purge', '/cache')
|
|
145
|
+
purgeCache() {
|
|
146
|
+
return { method: 'PURGE' };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`@Route(...)`는 비어 있지 않은 HTTP token을 받고 metadata 등록 전에 uppercase로 canonicalize하며, whitespace, separator, control character, non-ASCII token character가 들어오면 `InvalidHttpMethodError`로 거부합니다. `ALL`은 framework-owned `@All(...)` wildcard 전용이므로 `@Route(...)`에서 거부됩니다. Custom method를 포함한 method-specific route는 `@All(...)`보다 먼저 매칭되고 duplicate detection과 route versioning에 참여하며, 일반 DTO binding, validation, guard, interceptor, response pipeline을 그대로 사용합니다. Status metadata가 별도로 지정되지 않으면 성공한 `QUERY` 및 extension-method handler는 `200`을 기본값으로 사용합니다.
|
|
152
|
+
|
|
153
|
+
Adapter wire support는 명시적인 portability contract입니다. 지원되는 Node listener, Fastify와 Express wildcard fallback, Bun, Deno, Cloudflare Workers fetch dispatch는 `QUERY`와 대표 extension method를 일반 method로 바꾸지 않고 실행합니다. Custom method는 Bun native `routes` 가속 대상에서 제외되며, Fastify는 wildcard fallback이 해당 요청을 받을 수 있도록 method 이름만 등록합니다. 두 경로 모두 native fluo route handoff를 만들지 않습니다. `CONNECT`는 일반 controller-route conformance 범위 밖에 남습니다.
|
|
154
|
+
|
|
155
|
+
Custom runtime method가 자동으로 OpenAPI Path Item operation이 되는 것은 아닙니다. `@fluojs/openapi`는 계속 문서화된 standard operation method만 받으므로 custom-method descriptor를 OpenAPI input에서 제외하거나 application-owned extension으로 해당 endpoint를 문서화해야 합니다.
|
|
156
|
+
|
|
157
|
+
### 이식 가능한 헤더 helper
|
|
158
|
+
|
|
159
|
+
미들웨어, versioning, DTO binding, controller 코드가 adapter가 넘긴 `string | string[] | undefined`
|
|
160
|
+
header 값을 납작하게 만들지 않으면서 case-insensitive lookup을 해야 하면
|
|
161
|
+
`getRequestHeader(request, name)`를 사용하세요.
|
|
162
|
+
|
|
163
|
+
응답 negotiation이나 cache 로직이 case variant를 중복하지 않고 `Vary` 필드를 추가해야 하거나,
|
|
164
|
+
comma list를 매번 수동으로 파싱하고 싶지 않거나, 기존 `Vary: *` contract를 실수로 확장하면 안
|
|
165
|
+
될 때는 `appendVaryHeader(response, ...fields)`를 사용하세요.
|
|
166
|
+
|
|
167
|
+
Adapter가 제공한 응답 header를 같은 방식으로 case-insensitive lookup하려면
|
|
168
|
+
`getResponseHeader(response, name)`와 `hasResponseHeader(response, name)`를 사용하세요. 두 helper는
|
|
169
|
+
원래의 `string | string[]` shape을 보존하고 header, body, status, commit state를 쓰지 않습니다.
|
|
170
|
+
|
|
171
|
+
`attachment` 또는 `inline` Content-Disposition field value는
|
|
172
|
+
`buildContentDisposition(disposition, filename)`으로 만드세요. 이 helper는 escape한 printable-ASCII
|
|
173
|
+
`filename` fallback과 deterministic RFC 8187 UTF-8 `filename*` 값을 함께 만들고, carriage return 또는
|
|
174
|
+
line feed가 있는 filename은 header value를 반환하기 전에 reject합니다.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import {
|
|
178
|
+
appendVaryHeader,
|
|
179
|
+
buildContentDisposition,
|
|
180
|
+
getRequestHeader,
|
|
181
|
+
getResponseHeader,
|
|
182
|
+
hasResponseHeader,
|
|
183
|
+
type RequestContext,
|
|
184
|
+
} from '@fluojs/http';
|
|
185
|
+
|
|
186
|
+
export function readLanguage(context: RequestContext): string | undefined {
|
|
187
|
+
const acceptLanguage = getRequestHeader(context.request, 'accept-language');
|
|
188
|
+
return Array.isArray(acceptLanguage) ? acceptLanguage[0] : acceptLanguage;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function markLanguageVariance(context: RequestContext): void {
|
|
192
|
+
appendVaryHeader(context.response, 'Accept-Language', 'Origin');
|
|
193
|
+
context.response.setHeader(
|
|
194
|
+
'Content-Disposition',
|
|
195
|
+
buildContentDisposition('attachment', 'résumé.pdf'),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function readResponseEtag(
|
|
200
|
+
context: RequestContext,
|
|
201
|
+
): string | string[] | undefined {
|
|
202
|
+
return getResponseHeader(context.response, 'etag');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function shouldSetResponseEtag(context: RequestContext): boolean {
|
|
206
|
+
return !hasResponseHeader(context.response, 'etag');
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Content negotiation
|
|
211
|
+
|
|
212
|
+
응답 formatter로 `ContentNegotiationOptions`를 구성한 다음, 각 route가 반환할 수 있는
|
|
213
|
+
representation은 `@Produces(...)`로 제한하세요. Dispatcher가 formatter 선택, response commit,
|
|
214
|
+
`Content-Type`, canonical 406 response, `Vary: Accept`를 소유하며 handler는 값만 반환합니다.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import { Controller, Get, Produces } from '@fluojs/http';
|
|
218
|
+
|
|
219
|
+
@Controller('/reports')
|
|
220
|
+
export class ReportController {
|
|
221
|
+
@Produces('application/json', 'text/plain')
|
|
222
|
+
@Get('/')
|
|
223
|
+
getReport() {
|
|
224
|
+
return { ok: true };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`Accept`가 없거나, blank이거나, `*/*`이면 구성한 default formatter를 사용합니다. 해당 formatter가
|
|
230
|
+
`@Produces(...)`에서 허용되지 않으면 선언 순서상 첫 번째 허용 formatter를 대신 사용합니다. Header가
|
|
231
|
+
있으면 exact range, `type/*`, `*/*`, `application/*+json` 같은 structured suffix range를
|
|
232
|
+
case-insensitive로 매칭합니다. 더 높은 `q`가 먼저이고, 다음은 더 specific한 range이며, 그 다음은
|
|
233
|
+
구성한 default(또는 formatter 선언 순서)입니다. 더 specific한 `q=0` range는 더 넓은 wildcard가
|
|
234
|
+
positive여도 해당 representation을 제외합니다.
|
|
235
|
+
|
|
236
|
+
각 `q` 값은 소수점 이하 세 자리 이하로 `0`과 `1` 사이여야 합니다. 잘못된 media range 또는 quality는
|
|
237
|
+
무시하며, 유효하고 허용되는 representation이 없거나 formatter가 허용·매칭되지 않으면 dispatcher는
|
|
238
|
+
canonical `406 Not Acceptable` response를 반환합니다. 성공한 모든 formatter 선택은 기존 `Vary` field를
|
|
239
|
+
보존하면서 canonical하고 중복 없는 `Vary: Accept` 하나를 작성합니다.
|
|
240
|
+
|
|
76
241
|
## 주요 패턴
|
|
77
242
|
|
|
78
243
|
### 가드와 인터셉터
|
|
@@ -91,6 +256,37 @@ class AdminController {
|
|
|
91
256
|
}
|
|
92
257
|
```
|
|
93
258
|
|
|
259
|
+
### Request observer
|
|
260
|
+
|
|
261
|
+
`onRequestSuccess`는 매칭된 handler와 모든 module-level 및 application-level middleware가 완전히 settle된 뒤에만 호출되며, 여기에는 `await next()` 이후의 작업도 포함됩니다. Middleware가 `next()` 반환 뒤 예외를 던지면 observer는 앞선 success 알림 없이 `onRequestError`를 받습니다. `onRequestFinish`는 어느 outcome에서든 그 뒤에 호출됩니다.
|
|
262
|
+
|
|
263
|
+
### Access logging
|
|
264
|
+
|
|
265
|
+
`createAccessLogObserver(...)`는 request-observer lifecycle을 애플리케이션 소유의 structured record로 변환합니다. Start record, dispatch error마다 error record, 그리고 monotonic duration, optional request ID, method, path, matched route, status, outcome(`success`, `handled_error`, `unhandled_error`, `not_found`, `aborted`)을 가진 terminal finish record 하나를 정확히 emit합니다. Observer가 구성되면 native route dispatch는 이 complete lifecycle으로 fallback합니다.
|
|
266
|
+
|
|
267
|
+
Sink는 의도적으로 consumer-owned입니다. `AccessLogEvent`를 애플리케이션의 structured logger, telemetry pipeline, 또는 운영 데이터 보존 정책으로 전달하세요. Allowlist에 없는 header는 emit하지 않습니다. Allowlist에 넣어도 `authorization`, `cookie`, `set-cookie`, `proxy-authorization`, `x-api-key`는 계속 redaction되며, 조직별 header 이름은 `redact`로 추가합니다.
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import { createAccessLogObserver } from '@fluojs/http';
|
|
271
|
+
|
|
272
|
+
const accessLogObserver = createAccessLogObserver({
|
|
273
|
+
clientIdentity: {},
|
|
274
|
+
headers: {
|
|
275
|
+
allow: ['user-agent', 'set-cookie'],
|
|
276
|
+
redact: ['x-tenant-token'],
|
|
277
|
+
},
|
|
278
|
+
sink: {
|
|
279
|
+
emit(event) {
|
|
280
|
+
structuredLog.write(event);
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
클라이언트 주소가 필요하지 않으면 `clientIdentity`를 생략하세요. `clientIdentity: {}`는 adapter의 direct transport peer를 명시적으로 opt-in하고 forwarding field를 무시합니다. 신뢰한 proxy boundary를 통해 forwarded identity를 사용해야 할 때만 `clientIdentity: { trustProxy: ['10.0.0.0/8'] }`를 사용하세요.
|
|
287
|
+
|
|
288
|
+
Request ID는 optional입니다. Observer만 설치하면 ID를 만들지 않으므로 observer-only record에는 `requestId`가 없을 수 있습니다. `createCorrelationMiddleware()`를 설치하면 dispatcher가 incoming `x-request-id` 또는 legacy `x-correlation-id`를 채택하고, 둘 다 없을 때 access-log start record 전에 ID를 생성합니다.
|
|
289
|
+
|
|
94
290
|
### 비동기 요청 컨텍스트
|
|
95
291
|
|
|
96
292
|
```ts
|
|
@@ -102,11 +298,132 @@ function someDeepHelper() {
|
|
|
102
298
|
}
|
|
103
299
|
```
|
|
104
300
|
|
|
105
|
-
`runWithRequestContext(...)`는 호스트가 `globalThis.AsyncLocalStorage` 또는
|
|
301
|
+
`runWithRequestContext(...)`는 호스트가 `globalThis.AsyncLocalStorage` 또는 `node:async_hooks` 모듈로 `AsyncLocalStorage`를 제공할 때 활성 컨텍스트를 `await` 이후까지 보존합니다. 루트 `@fluojs/http` export는 async-context storage를 probe하거나 instantiate하지 않고 runtime-specific entrypoint를 선택합니다. Node와 Bun은 module initialization 중 host constructor를 등록하고, Deno, worker, browser, default entry는 Node built-in import 없이 유지됩니다. Request-local store 자체는 첫 사용 시점에 계속 lazy하게 생성됩니다. Promise를 반환하는 non-async callback은 동기 호출, 반환, throw 동작을 유지하고, 반환한 promise가 settle될 때까지 continuation에서 바인딩된 context를 보존합니다. Helper는 `Promise.prototype.then`을 교체하지 않으므로 관련 없는 promise continuation이 request를 capture하지 않습니다. 비동기 컨텍스트 primitive가 없는 호스트는 awaited work가 재개되기 전에 context를 지우는 synchronous-only fallback을 사용합니다.
|
|
302
|
+
|
|
303
|
+
## 응답 쿠키
|
|
304
|
+
|
|
305
|
+
adapter 고유 응답 API 대신 이식 가능한 `setCookie()`와 `clearCookie()` helper를 사용하세요. 각 호출은 독립적인 `Set-Cookie` field 하나를 작성하므로, 반복 호출은 순서를 보존하며 comma-folding되지 않습니다.
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import { clearCookie, setCookie } from '@fluojs/http';
|
|
309
|
+
|
|
310
|
+
setCookie(context.response, 'session', sessionToken, {
|
|
311
|
+
httpOnly: true,
|
|
312
|
+
maxAgeSeconds: 60 * 60,
|
|
313
|
+
path: '/',
|
|
314
|
+
sameSite: 'lax',
|
|
315
|
+
secure: true,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
clearCookie(context.response, 'session', {
|
|
319
|
+
path: '/',
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
`maxAgeSeconds`는 모든 adapter에서 음수가 아닌 정수 초 단위 lifetime입니다. 값은 percent-encoding되고, 이름과 attribute는 응답이 변경되기 전에 검증되며, `sameSite: 'none'`에는 `secure: true`가 필요합니다. 같은 browser cookie를 삭제하려면 기존 `path`와 `domain`을 반복해야 합니다. `httpOnly`, `secure`, `sameSite`는 browser matching key가 아니라 policy attribute입니다.
|
|
324
|
+
|
|
325
|
+
## Early Hints
|
|
326
|
+
|
|
327
|
+
`FrameworkResponse.earlyHints`는 HTTP `103` informational response를 위한 optional request-scoped capability입니다. 사용 전에 property 존재 여부를 확인하세요. Property가 없으면 active adapter가 Early Hints를 emit할 수 없다는 뜻입니다. 필수 `FrameworkResponse.writeEarlyHints()` method는 없으며 unsupported adapter가 write를 조용히 무시하지도 않습니다.
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
import type { RequestContext } from '@fluojs/http';
|
|
331
|
+
|
|
332
|
+
async function render(_input: undefined, context: RequestContext) {
|
|
333
|
+
const earlyHints = context.response.earlyHints;
|
|
334
|
+
|
|
335
|
+
if (earlyHints) {
|
|
336
|
+
await earlyHints.write({
|
|
337
|
+
link: [
|
|
338
|
+
'</styles.css>; rel=preload; as=style',
|
|
339
|
+
'</app.js>; rel=modulepreload',
|
|
340
|
+
],
|
|
341
|
+
'x-trace-id': 'render-1',
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
context.response.setHeader('link', '</final.css>; rel=stylesheet');
|
|
346
|
+
return { ok: true };
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
각 `write(...)`는 하나의 `103`을 emit하므로 final response 전에 여러 write를 순서대로 await할 수 있습니다. 모든 write에는 비어 있지 않은 `link` value가 하나 이상 필요하며 유효한 HTTP name과 value를 사용하는 추가 informational field를 포함할 수 있습니다. Header name은 대소문자를 구분하지 않으며 casing만 다른 이름을 중복해서 사용할 수 없습니다. Status상 금지된 framing field(`content-length`, `transfer-encoding`)는 native write 전에 reject됩니다. Early field는 `response.headers`를 채우거나 status를 바꾸거나 `committed`를 설정하지 않으며 final-response header로 복사되지도 않습니다.
|
|
351
|
+
|
|
352
|
+
Node.js, Express, Fastify는 이 capability를 노출합니다. Fetch-style Web, Bun, Deno, Cloudflare Workers response는 해당 `Response` API로 final response 이전 informational response를 표현할 수 없으므로 capability를 생략합니다. Final commit 이후 write 또는 native validation/write 실패는 `EarlyHintsWriteError`(`EARLY_HINTS_WRITE_FAILED`)로 reject되고, settlement 전에 연결이 끊기면 `RequestAbortedError`(`REQUEST_ABORTED`)로 reject됩니다.
|
|
353
|
+
|
|
354
|
+
## Realtime Adapter Capabilities
|
|
355
|
+
|
|
356
|
+
`HttpApplicationAdapter.getRealtimeCapability()`는 platform이 realtime protocol integration에서 server-backed, fetch-style, unsupported 중 무엇인지 보고합니다. Fetch-style capability는 version 1을 유지합니다. Host는 stable capability discriminator를 바꾸지 않으면서 first-party realtime package가 adapter `listen()` 전에 binding을 설치할 수 있도록 별도로 versioned된 optional `bindingInstallation` extension을 노출할 수 있습니다.
|
|
357
|
+
|
|
358
|
+
`createFetchStyleHttpAdapterRealtimeCapability(reason, options)`는 항상 source-compatible version 1 capability를 반환합니다. Installer를 제공하면 반환값에 `bindingInstallation`도 포함됩니다. Installer는 protocol-owned binding 또는 pre-listen cleanup을 위한 `undefined`를 받으며 platform adapter는 이 boundary를 host-specific binding type으로 parse할 책임이 있습니다. Managed adapter가 live 상태가 된 뒤에는 최종 binding cleanup을 adapter `close()` boundary가 소유합니다. Application code는 일반적으로 이 low-level adapter capability를 직접 호출하지 말고 `@fluojs/websockets` 또는 `@fluojs/socket.io` module을 등록해야 합니다.
|
|
359
|
+
|
|
360
|
+
## HTTP Error Representations
|
|
361
|
+
|
|
362
|
+
Canonical JSON이 default error response로 유지된다. Browser request에 API client 동작을 바꾸지 않고 complete
|
|
363
|
+
error/not-found document를 제공하려면 runtime bootstrap에 optional application-owned HTML provider를 등록한다.
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
import type { HttpErrorRepresentationOptions } from '@fluojs/http';
|
|
367
|
+
import { bootstrapApplication } from '@fluojs/runtime';
|
|
368
|
+
|
|
369
|
+
function escapeHtml(value: string): string {
|
|
370
|
+
return value
|
|
371
|
+
.replaceAll('&', '&')
|
|
372
|
+
.replaceAll('<', '<')
|
|
373
|
+
.replaceAll('>', '>')
|
|
374
|
+
.replaceAll('"', '"')
|
|
375
|
+
.replaceAll("'", ''');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const errorRepresentation = {
|
|
379
|
+
html: {
|
|
380
|
+
canRender({ request }) {
|
|
381
|
+
return request.method === 'GET' || request.method === 'HEAD';
|
|
382
|
+
},
|
|
383
|
+
render({ json }) {
|
|
384
|
+
return `<!doctype html><main>${json.error.status}: ${escapeHtml(json.error.message)}</main>`;
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
} satisfies HttpErrorRepresentationOptions;
|
|
388
|
+
|
|
389
|
+
const app = await bootstrapApplication({
|
|
390
|
+
errorRepresentation,
|
|
391
|
+
rootModule: AppModule,
|
|
392
|
+
});
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
HTTP가 representation selection 전에 outcome을 분류한다. Route miss는 기존 404 outcome이 되고 middleware,
|
|
396
|
+
DTO binding/validation, guard, interceptor, handler의 uncommitted `HttpException`은 같은 seam을 사용한다.
|
|
397
|
+
Provider는 classified exception, canonical `ErrorResponse`, request, optional matched handler, request id, active
|
|
398
|
+
request-scope container를 받는다. `FrameworkResponse`는 받지 않으므로 status, header, `HEAD`, abort, commit
|
|
399
|
+
ownership은 dispatcher에 남는다.
|
|
400
|
+
|
|
401
|
+
Provider return value는 application이 책임지는 trusted HTML이다. fluo는 이를 escape하거나 sanitize하지 않는다.
|
|
402
|
+
예제의 `json.error.message`처럼 request-derived 또는 error-derived value를 interpolation 전에 모두 HTML escape하거나,
|
|
403
|
+
text-node contract가 해당 escape를 수행하는 rendering framework를 사용해야 한다.
|
|
404
|
+
|
|
405
|
+
`Accept` negotiation은 deterministic하다. `Accept`가 없거나 wildcard/tie이면 JSON을 선택하고 quality와
|
|
406
|
+
specificity가 `application/json`과 available `text/html` 사이를 선택하며 unsupported range는 canonical JSON
|
|
407
|
+
406을 만든다. `canRender(...)`로 application 또는 matched handler별 HTML availability를 제한할 수 있다.
|
|
408
|
+
Provider failure는 원래 canonical JSON outcome으로 한 번만 fallback하며 committed 또는 aborted request는
|
|
409
|
+
다시 쓰지 않는다. Response writer `send(...)` 또는 stream/write failure는 그대로 propagate하며 두 번째 canonical
|
|
410
|
+
JSON write를 시작하지 않는다. HTTP가 `Accept`를 추가할 때 기존 native `Vary` 값도 보존한다. Successful-route
|
|
411
|
+
`@Produces(...)` metadata는 error representation을 제어하지 않는다. 전체 phase/fallback 계약은
|
|
412
|
+
[HTTP error representation decision](../../docs/architecture/http-error-representations.ko.md)을 참고한다.
|
|
106
413
|
|
|
107
414
|
### 프록시 뒤의 속도 제한
|
|
108
415
|
|
|
109
|
-
`createRateLimitMiddleware(...)`는 기본적으로
|
|
416
|
+
`createRateLimitMiddleware(...)`는 기본적으로 어댑터가 snapshot한 직접 transport 주소로 클라이언트 식별자를 해석합니다. `Forwarded`, `X-Forwarded-For`, `X-Real-IP`를 신뢰하려면 명시적인 hop 수, address/CIDR 목록 또는 predicate를 `trustProxy`로 구성하세요. 직접 peer가 해당 policy를 만족하지 않으면 forwarded data는 무시되며, malformed `Forwarded` data는 direct transport identity로 fail-closed 됩니다.
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
import { resolveHttpConnection } from '@fluojs/http';
|
|
420
|
+
|
|
421
|
+
const connection = resolveHttpConnection(context.request, {
|
|
422
|
+
trustProxy: ['10.0.0.0/8', '2001:db8:feed::/48'],
|
|
423
|
+
});
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
`connection`은 immutable이며 선택된 `clientAddress`, direct `remoteAddress`, 신뢰된 `proxyChain`, `protocol`, `secure`, `host`, `hostname`, `port`를 노출합니다. Fetch-only adapter는 Web `Request` contract가 direct address를 제공하지 않으므로 해당 값을 undefined로 둘 수 있습니다. adapter가 제공한 `connection` snapshot이나 명시적 header가 없는 fetch-style HTTPS `Request`는 peer, host, port를 갖지 않으며 `resolveHttpConnection(...)`은 URL에서 HTTPS, `secure`, host, port를 추론하지 않습니다. 기존 `trustProxyHeaders: true` 설정은 광범위한 compatibility 전용이며 새 deployment에는 권장하지 않습니다. deployment boundary는 `trustProxy`로 정확히 기술하세요. 두 설정 모두 해당 header를 다시 쓰는 proxy를 제어할 때만 사용해야 합니다. 어댑터가 신뢰 가능한 proxy chain과 raw socket identity를 모두 제공하지 않는다면 shared fallback bucket 대신 명시적인 `keyResolver`를 설정하세요.
|
|
110
427
|
|
|
111
428
|
### 서버 전송 이벤트
|
|
112
429
|
|
|
@@ -127,7 +444,9 @@ export class OrdersEventsController {
|
|
|
127
444
|
}
|
|
128
445
|
```
|
|
129
446
|
|
|
130
|
-
`@Sse(path)`는 `GET` 라우트를 등록하고 `text/event-stream` produced media type metadata를 선언합니다. Handler는 수동 stream 제어가 필요하면 `SseResponse`를 반환할 수 있고, managed streaming이 필요하면 `AsyncIterable<SseMessage<T> | T>`를 반환할 수 있습니다. Managed async iterable은 `SseResponse`와 같은 `encodeSseMessage(...)` 동작으로 변환됩니다. 일반 yield 값은 `data:` frame이 되고, `data` 필드가 있는 객체는 `event`, `id`, `retry`도 함께 제공할 수 있습니다. Dispatcher는 `RequestContext.request.signal`이 abort되거나 response stream이 닫히면 source 소비를 중단하고, write가 backpressure를 보고하면 `FrameworkResponseStream.waitForDrain()`을 기다리며, 완료 또는 source error 시 stream을
|
|
447
|
+
`@Sse(path)`는 `GET` 라우트를 등록하고 `text/event-stream` produced media type metadata를 선언합니다. Handler는 수동 stream 제어가 필요하면 `SseResponse`를 반환할 수 있고, managed streaming이 필요하면 `AsyncIterable<SseMessage<T> | T>`를 반환할 수 있습니다. 수동 `SseResponse`는 명시적 close, request abort 또는 raw stream close까지 dispatch, request observer, request-scoped resource를 유지하고, 그 lifecycle stage는 이후 정확히 한 번 해제됩니다. Managed async iterable은 `SseResponse`와 같은 `encodeSseMessage(...)` 동작으로 변환됩니다. 일반 yield 값은 `data:` frame이 되고, `data` 필드가 있는 객체는 `event`, `id`, `retry`도 함께 제공할 수 있습니다. Dispatcher는 `RequestContext.request.signal`이 abort되거나 response stream이 닫히면 source 소비를 중단하고, write가 backpressure를 보고하면 `FrameworkResponseStream.waitForDrain()`을 기다리며, 완료 또는 source error 시 stream을 닫습니다. 같은 cancellation boundary가 진행 중인 `waitForDrain()`도 제한합니다. Request abort 또는 stream close는 settle되지 않은 drain promise보다 먼저 완료되고, 이후 dispatcher는 source iterator를 정확히 한 번 닫은 다음 request-scope disposal을 계속합니다. Stream write failure와 reject된 drain promise는 원래 error를 그대로 전파합니다. 취소 시에는 response stream을 즉시 닫고 request-scoped resource를 dispose하기 전에 source iterator의 `return()` cleanup을 기다립니다. Cleanup 실패는 이미 commit된 SSE response를 대체하지 않고 request observer와 dispatcher logger seam으로 보고됩니다. Source에서 던진 오류도 같은 committed-response error/observer 경계를 따릅니다. Observable 값은 계속 범위 밖이며 RxJS dependency는 필요하지 않습니다.
|
|
448
|
+
|
|
449
|
+
Managed SSE는 `FrameworkResponse.stream`을 노출하는 adapter가 필요합니다. 활성 adapter가 response stream을 제공하지 않으면 dispatcher는 response를 처리된 것으로 표시하기 전에 managed async iterable을 거부하고, stream이 처리된 것으로 조용히 보고하는 대신 표준 dispatch error 경로(request error observer와 구성된 error response writer)를 통해 실패를 전달합니다.
|
|
131
450
|
|
|
132
451
|
브라우저 쪽에서는 해당 연결을 소유하는 React effect 안에서 `EventSource`를 만들고 cleanup 함수에서 항상 닫아야 합니다. 그래야 route 변경, Strict Mode remount, component unmount가 중복 stream을 남기지 않습니다.
|
|
133
452
|
|
|
@@ -174,7 +493,7 @@ Framework integration이 명시적인 request context boundary나 typed per-requ
|
|
|
174
493
|
|
|
175
494
|
### Fast-path observability
|
|
176
495
|
|
|
177
|
-
Dispatcher는 adapter와 diagnostics를 위해 `FAST_PATH_ELIGIBILITY_SYMBOL`, `FAST_PATH_STATS_SYMBOL`, `formatFastPathStats(...)`, `getDispatcherFastPathStats(...)`로 fast-path observability를 노출합니다.
|
|
496
|
+
Dispatcher는 adapter와 diagnostics를 위해 `FAST_PATH_ELIGIBILITY_SYMBOL`, `FAST_PATH_STATS_SYMBOL`, `formatFastPathStats(...)`, `getDispatcherFastPathStats(...)`로 fast-path observability를 노출합니다. Eligibility 결정은 shared `HandlerMapping`이 아니라 dispatcher instance에 속합니다. 따라서 여러 dispatcher가 서로 다른 middleware, observer, interceptor, binder, adapter option으로 하나의 mapping을 재사용해도 서로의 결정을 덮어쓰지 않습니다. `describeRoutes()`는 cloned descriptor에 frozen eligibility snapshot을 노출하며 dispatcher statistics와 그 route entry도 frozen observability value입니다.
|
|
178
497
|
|
|
179
498
|
### Bun decorator bundling compatibility
|
|
180
499
|
|
|
@@ -184,32 +503,128 @@ Fluo의 HTTP 데코레이터는 TC39 표준 데코레이터이며, runtime 또
|
|
|
184
503
|
|
|
185
504
|
## 요청 정리와 런타임 이식성
|
|
186
505
|
|
|
187
|
-
디스패처는 활성 dispatch 동안에만 호스트 비동기 컨텍스트 저장소로 `RequestContext`를 바인딩합니다. 지원되는 Node 20+ 런타임을 포함해 `AsyncLocalStorage`가 있는 호스트에서는 컨텍스트가 awaited work 이후까지 유지됩니다. 비동기 컨텍스트 primitive가 없는 비 Node 호스트에서는 fallback 컨텍스트가 동기 프레임에만 유효하고, 겹치는 요청이 서로의 컨텍스트를 관찰하지 않도록 `await` 이후에는 의도적으로 사용할 수 없습니다. 요청이 controller graph, middleware, guard, interceptor, observer, DTO converter, custom binder 또는 수동 `getCurrentRequestContext()` / `assertRequestContext()` container 접근을 통해 request-scoped DI를 사용할 수 있으면, 디스패처는 요청 observer가 끝난 뒤 `finally` 경로에서 isolated request-scoped DI 컨테이너를 생성하고 dispose합니다.
|
|
506
|
+
디스패처는 활성 dispatch 동안에만 호스트 비동기 컨텍스트 저장소로 `RequestContext`를 바인딩합니다. 지원되는 Node 20+ 런타임을 포함해 `AsyncLocalStorage`가 있는 호스트에서는 컨텍스트가 awaited work 이후까지 유지됩니다. 비동기 컨텍스트 primitive가 없는 비 Node 호스트에서는 fallback 컨텍스트가 동기 프레임에만 유효하고, 겹치는 요청이 서로의 컨텍스트를 관찰하지 않도록 `await` 이후에는 의도적으로 사용할 수 없습니다. 요청이 controller graph, middleware, guard, interceptor, observer, DTO converter, custom binder 또는 수동 `getCurrentRequestContext()` / `assertRequestContext()` container 접근을 통해 request-scoped DI를 사용할 수 있으면, 디스패처는 요청 observer가 끝난 뒤 `finally` 경로에서 isolated request-scoped DI 컨테이너를 생성하고 dispose합니다. Graph가 request scope를 필요로 하지 않는 route는 `RequestContext.container`가 접근되기 전까지 이 컨테이너 lifecycle을 건너뛰어 baseline 경로의 불필요한 per-request allocation을 피하면서도, graph가 모호하거나 request-scoped이면 request-scoped provider isolation을 유지합니다. Fast path는 handler metadata만 cache하고 매 dispatch마다 active container를 통해 controller를 resolve합니다. 따라서 singleton provider는 container에 의해 공유되고 transient controller와 dependency는 resolution마다 새로운 identity를 유지합니다. 그러므로 공개 `RequestContext.container` 읽기는 request-scoped provider resolve에 항상 안전합니다. Request-scope-free fast path는 내부 dispatcher 최적화일 뿐, 공개 context가 root container를 노출한다는 약속이 아닙니다.
|
|
507
|
+
|
|
508
|
+
어댑터는 플랫폼이 제공한다면 `FrameworkRequest.signal`에 `AbortSignal`을 전달하고, signal allocation이 실용적이지 않다면 `isAborted()` probe를 제공해야 합니다. Dispatcher는 per-dispatch request clone에 두 abort surface를 모두 보존하고 어느 한쪽이라도 cancellation을 보고하면 request를 aborted로 처리하므로 `false` probe가 aborted signal을 가리지 않습니다. Handler 작업 전후에 두 surface를 검사하므로 `AbortSignal`이 없는 어댑터도 abandon된 요청을 중단할 수 있습니다. SSE에서는 가능하면 `FrameworkResponse.stream.onClose(...)`도 노출해야 합니다. `SseResponse`는 request abort와 raw stream close를 모두 구독하고, 멱등하게 닫히며, 어느 쪽이 먼저 종료되더라도 등록한 listener를 제거합니다.
|
|
509
|
+
|
|
510
|
+
Multipart upload를 parse하는 어댑터는 shared HTTP contract를 adapter-specific file type으로 augment하지 말고 runtime-neutral `FrameworkRequestFile` 값을 `FrameworkRequest.files`에 붙여야 합니다. 이 seam은 모든 HTTP adapter가 제공할 수 있는 portable field(`fieldname`, `originalname`, `mimetype`, `buffer`, `size`)만 의도적으로 모델링합니다. Platform package는 더 풍부한 native file object를 raw request surface에 유지할 수 있지만, guard, binder, middleware, interceptor, controller가 cross-runtime 동작을 필요로 하면 `RequestContext.request.files`를 통해 파일을 읽어야 합니다.
|
|
511
|
+
|
|
512
|
+
### Multipart DTO 필드
|
|
513
|
+
|
|
514
|
+
Multipart 파일이 handler 입력 계약에 포함되면 `@RequestDto(...)`와 함께 `@FromFiles(fieldname?)`를 사용하세요.
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
import {
|
|
518
|
+
Controller,
|
|
519
|
+
FromFiles,
|
|
520
|
+
Optional,
|
|
521
|
+
Post,
|
|
522
|
+
RequestDto,
|
|
523
|
+
type FrameworkRequestFile,
|
|
524
|
+
} from '@fluojs/http';
|
|
525
|
+
|
|
526
|
+
class UploadAssetsDto {
|
|
527
|
+
@FromFiles('attachments')
|
|
528
|
+
attachments: readonly FrameworkRequestFile[] = [];
|
|
529
|
+
|
|
530
|
+
@FromFiles('cover')
|
|
531
|
+
@Optional()
|
|
532
|
+
cover?: readonly FrameworkRequestFile[];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
@Controller('/uploads')
|
|
536
|
+
export class UploadController {
|
|
537
|
+
@Post('/')
|
|
538
|
+
@RequestDto(UploadAssetsDto)
|
|
539
|
+
upload(input: UploadAssetsDto) {
|
|
540
|
+
return input.attachments.map((file) => file.originalname);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
`@FromFiles(...)`는 array-only입니다. `FrameworkRequest.files`가 있으면 `fieldname`으로 필터링된 readonly 배열을 어댑터 도착 순서대로 반환하며, collection이 있지만 일치 항목이 없으면 `[]`가 됩니다. Collection이 없으면 필수 필드는 표준 missing-field 오류를 내고 `@Optional()` 필드는 `undefined`로 남습니다. Converter와 validation은 같은 portable 배열을 받습니다. DTO binder는 다섯 `FrameworkRequestFile` 필드만 projection하므로 adapter-native file property가 DTO 경계를 넘어오지 않습니다. 전체 요청 collection이 필요한 controller와 pipeline stage에서는 기존처럼 `RequestContext.request.files`에 직접 접근할 수 있습니다.
|
|
188
546
|
|
|
189
|
-
|
|
547
|
+
응답 content negotiation formatter는 `ResponseFormatter.format(...)`에서 `string` 또는 `Uint8Array`를 반환해야 합니다. Node.js `Buffer` 값은 `Buffer`가 `Uint8Array`를 구현하므로 계속 할당 가능하지만, formatter contract는 runtime-neutral byte 동작에만 의존해야 합니다.
|
|
190
548
|
|
|
191
549
|
## 공개 API
|
|
192
550
|
|
|
193
|
-
- **라우팅 데코레이터**: `Controller`, `Get`, `Sse`, `Post`, `Put`, `Patch`, `Delete`, `All`, `Options`, `Head`
|
|
194
|
-
- **바인딩 데코레이터**: `FromBody`, `FromQuery`, `FromPath`, `FromHeader`, `FromCookie`, `RequestDto`, `Optional`, `Convert`
|
|
551
|
+
- **라우팅 데코레이터**: `Controller`, `Get`, `Sse`, `Query`, `Route`, `Post`, `Put`, `Patch`, `Delete`, `All`, `Options`, `Head`
|
|
552
|
+
- **바인딩 데코레이터**: `FromBody`, `FromQuery`, `FromPath`, `FromHeader`, `FromCookie`, `FromFiles`, `RequestDto`, `Optional`, `Convert`
|
|
195
553
|
- **실행 데코레이터**: `UseGuards`, `UseInterceptors`, `HttpCode`, `Version`, `Header`, `Redirect`, `Produces`
|
|
196
|
-
-
|
|
197
|
-
- **
|
|
198
|
-
-
|
|
199
|
-
-
|
|
554
|
+
- **응답 쿠키 helper**: `setCookie`, `clearCookie`, `CookieOptions`, `ClearCookieOptions`, `CookieSameSite`
|
|
555
|
+
- **Conditional request 타입**: `EntityTagStrength`, `EntityTag`, `ResponseValidators`, `ConditionalRequestContext`, `ConditionalRequestResolution`, `ConditionalRequestResolver`, `ConditionalRequestOptions`
|
|
556
|
+
- **바이트 범위 응답**: `createByteRangeResponse`, `ByteRangeResponseSource`, `ByteRangeResponseOptions`
|
|
557
|
+
- **정적 에셋**: `createStaticAssetsMiddleware`, `StaticAssetSource`, `StaticAsset`, `StaticAssetAcceptedEncoding`, `StaticAssetContentEncoding`, `StaticAssetNotAcceptable`, `StaticAssetResolveContext`, `StaticAssetResolution`, `StaticAssetsMiddleware`, `StaticAssetsMiddlewareOptions`. 이 패키지는 middleware와 source-selection contract를 소유하며 선택적인 Node filesystem source는 `@fluojs/platform-nodejs`가 소유합니다.
|
|
558
|
+
- **응답 transport 제어**: `FrameworkResponseSendOptions`, `FrameworkResponseStream` (`onError`는 `undefined`를 포함한 모든 transport failure 발생을 보고하고, stream이 settle된 뒤 caller가 호출할 optional remover를 반환함)
|
|
559
|
+
- **요청/응답 및 컨텍스트 타입**: `RequestContext`, `Principal`, `ContextKey`, `ControllerHandler`, `FrameworkRequest`, `FrameworkRequestFile`, `FrameworkResponse`, `EarlyHintsHeaders`, `FrameworkResponseEarlyHints`, `FrameworkResponseStream`, `FrameworkResponseCompression`, `FrameworkResponseCompressionWriteOptions`, `SseResponse`, `SseMessage`
|
|
560
|
+
- **신뢰된 연결 API**: `resolveHttpConnection`, `HttpConnection`, `ResolveHttpConnectionOptions`, `TrustProxyPolicy`, `TrustProxyPredicate`, `FrameworkRequestConnection`
|
|
561
|
+
- **구조화된 접근 로깅**: `createAccessLogObserver`, `CreateAccessLogObserverOptions`, `AccessLogSink`, `AccessLogEvent`, `AccessLogStartEvent`, `AccessLogErrorEvent`, `AccessLogFinishEvent`, `AccessLogOutcome`, `AccessLogHeaderOptions`, `AccessLogRequestFields`
|
|
562
|
+
- **디스패처, 라우팅, 협상 타입**: `Dispatcher`, `CreateDispatcherOptions`, `ErrorHandler`, `DispatcherLogger`, `HandlerMapping`, `HandlerMetadata`, `HandlerDescriptor`, `HandlerMatch`, `HandlerSource`, `RouteDefinition`, `HttpMethod`, `VersioningType`, `VersioningOptions`, `VersioningExtractor`, `VersioningExtractorResult`, `ContentNegotiationOptions`, `ResponseFormatter`, `HttpErrorRepresentationContext`, `HtmlErrorRepresentationProvider`, `HttpErrorRepresentationOptions`, `FastPathEligibility`, `FastPathStats`
|
|
563
|
+
- **파이프라인 계약 타입**: `Middleware`, `MiddlewareLike`, `MiddlewareContext`, `MiddlewareRouteConfig`, `Next`, `Guard`, `GuardLike`, `GuardContext`, `Interceptor`, `InterceptorLike`, `InterceptorContext`, `CallHandler`, `RequestObserver`, `RequestObserverLike`, `RequestObservationContext`, `ArgumentResolverContext`, `Binder`, `Converter`, `ConverterLike`, `ConverterTarget`, `ValidationIssue`, `Validator`
|
|
564
|
+
- **Adapter API**: `HttpApplicationAdapter`, `HttpAdapterRealtimeCapability`, `ServerBackedHttpAdapterRealtimeCapability`, `FetchStyleHttpAdapterRealtimeCapability`, `HttpAdapterRealtimeBindingInstallation`, `UnsupportedHttpAdapterRealtimeCapability`, `createNoopHttpApplicationAdapter`, `createServerBackedHttpAdapterRealtimeCapability`, `createUnsupportedHttpAdapterRealtimeCapability`, `createFetchStyleHttpAdapterRealtimeCapability`
|
|
565
|
+
- **예외와 오류**: `HttpExceptionDetail`, `HttpExceptionOptions`, `ErrorResponse`, `HttpException`, `BadRequestException`, `UnauthorizedException`, `ForbiddenException`, `NotFoundException`, `ConflictException`, `NotAcceptableException`, `TooManyRequestsException`, `InternalServerErrorException`, `PayloadTooLargeException`, `createErrorResponse`, `RouteConflictError`, `InvalidRoutePathError`, `InvalidHttpMethodError`, `HandlerNotFoundError`, `RequestAbortedError`, `EarlyHintsWriteError`
|
|
566
|
+
- **헬퍼**: `createHandlerMapping`, `createDispatcher`, `forRoutes`, `normalizeRoutePattern`, `matchRoutePattern`, `isMiddlewareRouteConfig`, `createCorrelationMiddleware`, `createCorsMiddleware`, `createRateLimitMiddleware`, `createMemoryRateLimitStore`, `createSecurityHeadersMiddleware`, `getRequestHeader`, `getResponseHeader`, `hasResponseHeader`, `appendVaryHeader`, `buildContentDisposition`, `runWithRequestContext`, `getCurrentRequestContext`, `assertRequestContext`, `createRequestContext`, `createContextKey`, `getContextValue`, `setContextValue`, `encodeSseComment`, `encodeSseMessage`, `isSseMessage`, `formatFastPathStats`, `getDispatcherFastPathStats`, `FAST_PATH_ELIGIBILITY_SYMBOL`, `FAST_PATH_STATS_SYMBOL`
|
|
200
567
|
- **Option 및 store type**: `CorsOptions`, `RateLimitOptions`, `RateLimitStore`, `RateLimitStoreEntry`, `SecurityHeadersOptions`, `SseSendOptions`
|
|
201
568
|
|
|
569
|
+
## Portable 서브경로 (`@fluojs/http/portable`)
|
|
570
|
+
|
|
571
|
+
Node `AsyncLocalStorage` bootstrap을 eager 초기화하지 않고 HTTP authoring contract가 필요한 runtime-neutral integration에서는 `@fluojs/http/portable`을 사용하세요. 이 경로는 지원되는 HTTP decorator, exception, request/response contract, authoring helper를 내보냅니다. Node request-context 동작이 필요한 Node 애플리케이션은 계속 root package를 import해야 합니다.
|
|
572
|
+
|
|
202
573
|
## 내부 서브경로 (`@fluojs/http/internal`)
|
|
203
574
|
|
|
204
|
-
`./internal` 서브경로는 플랫폼
|
|
575
|
+
`./internal` 서브경로는 플랫폼 어댑터, 핵심 런타임, first-party response integration에서 사용하는 저수준 유틸리티만 내보냅니다. 이들은 변경될 수 있으며 일반적인 애플리케이션 코드에서 사용해서는 안 됩니다.
|
|
205
576
|
|
|
206
577
|
- `DefaultBinder`: 런타임 부트스트랩 경로에서 사용하는 기본 DTO/요청 바인더.
|
|
207
578
|
- `bindRawRequestNativeRouteHandoff(...)` / `attachFrameworkRequestNativeRouteHandoff(...)`: public dispatcher API를 넓히지 않고 의미 보존이 가능한 native route match를 재사용하기 위한 내부 adapter/runtime 헬퍼.
|
|
208
579
|
- `consumeRawRequestNativeRouteHandoff(...)` / `readFrameworkRequestNativeRouteHandoff(...)`: native route handoff를 읽거나 소비하기 위한 내부 helper.
|
|
209
580
|
- Native route handoff는 framework request에 붙는 시점의 method와 path를 함께 스냅샷합니다. app middleware가 handler matching 전에 둘 중 하나를 rewrite하면 dispatcher는 stale handoff를 무시하고 일반 route matching으로 fallback합니다.
|
|
210
581
|
- `isRoutePathNormalizationSensitive(path)`: duplicate slash와 trailing slash 요청을 generic dispatcher 경로에 남기기 위한 내부 guard.
|
|
582
|
+
- `getCompiledRouteIdentity(descriptor)`: first-party package integration을 위해 `createHandlerMapping(...)`이 할당한 deterministic source/method position을 읽습니다. 수동으로 작성한 descriptor에는 `undefined`를 반환합니다.
|
|
211
583
|
- `resolveClientIdentity(request)`: 속도 제한과 런타임 통합에서 사용하는 보수적 클라이언트 식별 해석기.
|
|
212
584
|
- `createFetchStyleHttpAdapterRealtimeCapability(...)`, `Dispatcher`, `HttpApplicationAdapter`: 전체 HTTP root barrel을 instantiate하면 안 되는 edge/fetch-style platform package를 위한 내부 adapter seam.
|
|
585
|
+
- `FRAMEWORK_RESPONSE_WRITER` / `registerFrameworkResponseWriter(...)`: first-party response integration을 위한 typed response-entry branding seam.
|
|
586
|
+
- `FRAMEWORK_RESPONSE_VALUE_FINALIZER` / `registerFrameworkResponseValueFinalizer(...)`: typed request-local response finalization seam. Finalizer는 registration 순서대로 compose되고 각각 이전에 resolve된 값을 받으며, dispatcher가 await하므로 throw와 rejection은 기존 error policy를 따릅니다.
|
|
587
|
+
|
|
588
|
+
## Conditional Requests
|
|
589
|
+
|
|
590
|
+
runtime bootstrap에서 `conditionalRequest`를 구성해 representation 존재 여부와 optional validator를 분리하여 해석합니다.
|
|
591
|
+
|
|
592
|
+
```ts
|
|
593
|
+
const app = await bootstrapNodeApplication(AppModule, {
|
|
594
|
+
conditionalRequest: {
|
|
595
|
+
resolve({ handler, request }) {
|
|
596
|
+
return {
|
|
597
|
+
exists: true,
|
|
598
|
+
validators: {
|
|
599
|
+
etag: { opaqueValue: `${handler.route.method}:${request.path}:v1`, strength: 'strong' },
|
|
600
|
+
lastModified: new Date('2026-01-01T00:00:00Z'),
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
representation이 없으면 `{ exists: false }`를, 존재하지만 validator가 의도적으로 없으면 `{ exists: true }`를 반환합니다. Dispatcher는 application/module middleware와 guard 뒤에 이 resolver를 평가하므로 conditional `304`와 `412`가 authorization 또는 audit logic을 우회하지 않습니다. 유효한 entity-tag list와 HTTP-date form만 받아들이며 malformed conditional field는 무시합니다.
|
|
609
|
+
|
|
610
|
+
dispatcher는 RFC 9110 precedence와 comparison을 소유합니다. 성공한 `If-Match`는 `If-Unmodified-Since`만 건너뛰고, 이후에도 `If-None-Match`가 `If-Modified-Since`보다 우선합니다. `If-Match`는 strong comparison, `If-None-Match`는 weak comparison을 사용합니다. `304`와 `412`는 body 없이 `ETag`/`Last-Modified`를 유지하며 redirect와 지원되는 custom response-writer 경로에도 적용됩니다. 같은 selected representation에서는 `HEAD`와 `GET`이 같은 conditional 결과를 사용하고 framework-generated `HEAD` body는 억제됩니다. 명시적인 `@Head` route는 독립 route이며 custom response writer는 body emission을 소유하므로 직접 bodyless `HEAD` contract를 지켜야 합니다. 전체 실행 계약은 [HTTP Runtime Contract](../../docs/architecture/http-runtime.ko.md)를 참고하세요.
|
|
611
|
+
|
|
612
|
+
## 바이트 범위 응답
|
|
613
|
+
|
|
614
|
+
`Uint8Array` 또는 `ArrayBuffer`를 반환하면 `GET`과 `HEAD` metadata mirror에서 RFC 단일 `bytes` 범위 응답이 자동으로 활성화됩니다. 유효한 범위는 `206`, `Accept-Ranges: bytes`, `Content-Range`, 정확한 identity-byte `Content-Length`를 생성합니다. malformed 또는 multi-range field는 전체 representation으로 fallback하고, 충족 불가능한 범위는 `Accept-Ranges: bytes`, `Content-Range: bytes */<size>`, `Content-Length: 0`을 포함한 body 없는 `416`을 반환합니다. `POST`, unsafe, custom method는 `Range`를 무시하고 원래 full status, body, metadata를 유지합니다.
|
|
615
|
+
|
|
616
|
+
portable `ReadableStream`에는 정확한 전체 크기와 함께 `createByteRangeResponse(...)`를 사용하세요. `HEAD`가 stream을 만들지 않아야 하면 factory를 전달합니다.
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
import { createByteRangeResponse } from '@fluojs/http';
|
|
620
|
+
|
|
621
|
+
return createByteRangeResponse(
|
|
622
|
+
() => file.stream(),
|
|
623
|
+
{ contentType: 'image/png', size: file.size },
|
|
624
|
+
);
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
dispatcher는 먼저 일반 conditional request를 평가합니다. 그 다음 `If-Range`는 정확한 strong `ETag` 또는 현재 `Last-Modified` date일 때만 partial response를 허용하며, 그 외에는 전체 representation을 보냅니다. partial response는 range offset과 length를 보존하도록 identity byte를 유지하고 Node compression을 건너뜁니다. `HEAD`는 GET의 status와 metadata를 보존하면서 stream을 열지 않습니다. Byte, 정확한 size, filesystem resource는 애플리케이션이 소유합니다. `createByteRangeResponse(...)`는 file을 열거나, stat, seek, size 계산, close하지 않으며 multi-range response도 의도적으로 구성하지 않습니다.
|
|
213
628
|
|
|
214
629
|
## 관련 패키지
|
|
215
630
|
|