1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
//! ユーザー向けインターセプト・レスポンス改変拡張点 `Interceptor`(イシュー #420)。
//!
//! # 背景・既存 3 拡張点で表現できない理由
//!
//! `crates/core` の既存 3 拡張点([`crate::extension`])はいずれも「リダイレクトを
//! 返す」「確定済みレスポンスの body を差し替える」ユースケースを表現できない:
//!
//! - [`crate::extension::Middleware`][]: 観測専用契約。`on_request` は `&RequestHead`
//! しか受け取らず、`on_response` / `on_response_with_status`(送出ステータスの
//! 数値のみ、イシュー #721)もレスポンスへの参照を持たない
//! - [`crate::extension::RequestGate`][]: `GateOutcome::Allow` / `Reject { status,
//! body }` の二択で、`Reject` はヘッダを運べないため 301 + `Location` を表現
//! できない
//! - [`crate::extension::UpgradeHandler`][]: 長時間接続への委譲判定専用
//!
//! `docs/spec/04-requirements.md` REQ-2 は「既存 3 拡張点で表現できない場合にのみ
//! 新規 trait の追加を検討する」と定めており、本 trait はこの例外条件を満たす
//! (設計判断の詳細は `docs/design/interceptor-extension-point.md`)。[`crate::server::Handler`]
//! が既に確立している「3 拡張点の対象外の既定レスポンダ差し込み口」という
//! 前例と同じ「レスポンダ系シーム」ファミリーとして位置づける。
//!
//! # 契約
//!
//! [`Middleware`][crate::extension::Middleware] と同じく同期 API(dyn 互換のため)。
//! 同期ブロッキング I/O を行ってはならない(PoC-3 実測でスループットが最大 25%
//! 劣化する、`crate::extension` モジュール doc・`AGENTS.md` を参照)。カスタム
//! 404 ページ等、レスポンス body に静的コンテンツを使う場合は起動時にメモリへ
//! プリロードしておく(下の doc test を参照)。
//!
//! # 評価順序(`crate::server::handle_connection_with_permit`)
//!
//! ```text
//! 1. Middleware::on_request
//! 2. RequestGate::check(フェイルクローズ。Reject 応答は Interceptor を一切通さない)
//! 3. UpgradeHandler::matches
//! 3.5. Interceptor::intercept(新規。登録順、最初の Some が勝つ)
//! 4. plugin::try_intercept(intercept が Some なら skip)
//! 5. Handler::handle / handle_streaming(同上 skip)
//! 5.4. Interceptor::map_response(新規。登録順に逐次適用。`handle_streaming` が
//! `Some` を返した経路では `crate::server::write_streaming_response` の
//! ヘッド確定時に同じく登録順で適用する。下の「ストリーミング応答への
//! 適用」節を参照、イシュー #434)
//! 5.5. plugin::finalize_response(CORS → 圧縮。map_response の後。ストリーミング
//! 応答には未適用のまま。ストリーミング応答には代わりに
//! plugin::finalize_streaming_head(CORS のみ、圧縮は対象外)が
//! `write_streaming_response` のヘッド確定時に適用される、イシュー #451。
//! 下の「ストリーミング応答への適用」節を参照)
//! 6. レスポンス書き込み → Middleware::on_response_with_status(既定実装が
//! `on_response` へ委譲するため、後者だけを実装する既存コードも動作する。
//! イシュー #721)
//! ```
//!
//! - **`RequestGate` より後**: ゲートの既定拒否(フェイルクローズ)をユーザー
//! コードで迂回できないようにする(A01 アクセス制御対策、`RequestGate` の
//! doc と同じ設計判断)
//! - **`UpgradeHandler` より後**: 確立済みの Upgrade 委譲・permit 引き継ぎ
//! 意味論(TASK-4.2)に触れない
//! - **`plugin::try_intercept` より前**: 利用者が登録済みプラグイン(例:
//! `plugin-static`)の応答をインターセプトで先取りできる(末尾スラッシュ
//! 301 正規化のユースケースが成立する条件)
//! - **`map_response` は `finalize_response`(CORS → 圧縮)より前**: CORS
//! ヘッダ付与・gzip 圧縮は改変後の最終 body に対して適用されるべきため
//!
//! # `map_response` を通さない応答(fail-closed)
//!
//! `finalize_response`(イシュー #305)と同一の設計判断として、以下の応答は
//! `Interceptor` の対象外とする:
//!
//! - `RequestGate` 拒否応答
//! - パースエラー応答(400 等、コネクション処理中に確定するもの)
//! - Upgrade 委譲失敗時の 501 応答・shutdown 中の 503 応答
//!
//! # ストリーミング応答への適用(ステータス・ヘッダのみ、body 破棄、イシュー #434)
//!
//! [`crate::server::Handler::handle_streaming`](イシュー #319)が返す応答は
//! `Response` 型を前提とする通常経路(上の 5.4)を通らないが、`map_response`
//! 自体は `crate::server::write_streaming_response` がヘッド確定時(HTTP/1.0・
//! HTTP/1.1 共通、1 回のみ)に登録順で適用する。ステータス・
//! `Content-Type`・追加ヘッダ(`Response::with_header` 等)の改変はここで
//! 反映されるが、ストリーミング応答の実体(body)は producer タスクが
//! [`crate::streaming::BodyWriter`] 経由で逐次供給し chunked framing は
//! コアが直接組み立てるため、`map_response` が返した `Response` の **body は
//! 反映されず破棄される**。body 差し替えを許すとバックプレッシャ(bounded
//! mpsc)・応答完全性契約(`finish` 省略時は終端チャンクなしで打ち切り
//! クローズ、`crate::streaming` モジュール doc の「応答完全性」節)と
//! 両立できず、body 全体のバッファリングが必要になり #319 の設計と矛盾する
//! ため、この制約は意図的な設計判断(新規フック追加・全 body バッファリング
//! は不採用、`docs/design/interceptor-extension-point.md` を参照)。
//!
//! `map_response` 適用後のステータスは以降のすべての判定(`Response::
//! is_bodyless_status` による 1xx/204/304 の body 送出スキップ含む)に一貫
//! 使用する。インターセプタが 200 → 204 等へ書き換えた場合、ヘッダ側の
//! framing 抑制と body 送出スキップが対で成立し、レスポンス分割類の脅威
//! (`Response::serialize_chunked_head` doc の RFC 9112 §6.3 コメントと同一
//! 脅威)を構造的に防ぐ。
//!
//! `plugin::finalize_response`(CORS → 圧縮を逐次適用する通常応答経路専用の
//! シーム)自体はストリーミング応答には引き続き適用しない。代わりにイシュー
//! #451 で `plugin::finalize_streaming_head`(`finalize_response` の第 4 の
//! シーム)を新設し、`write_streaming_response` がヘッド確定時(上の
//! `map_response` 適用の直後)に CORS ヘッダ付与のみを適用する。圧縮は
//! gzip がストリーム body 全体を確定させる後処理であり、chunked framing を
//! 直接組み立てるストリーミング設計(バックプレッシャ・打ち切りクローズ
//! 契約)と両立できないため引き続き対象外(`crate::plugin::
//! finalize_streaming_head` の doc を参照)。
//!
//! # pay-for-what-you-use との整合
//!
//! feature ゲート **不要**。外部依存ゼロの純コア機能であり、`Handler`・3 拡張点
//! と同じく実装ゼロなら実行時コストもゼロ(未登録時は空 `Vec` の走査のみ)。
//!
//! # Examples
//!
//! 末尾スラッシュを 301 で正規化する `intercept` の例:
//!
//! ```
//! use fandhe_backend_core::interceptor::Interceptor;
//! use fandhe_backend_http::request::{ParseOutcome, RequestHead, parse_request_head};
//! use fandhe_backend_http::response::Response;
//!
//! /// `/foo/` → `/foo` へ 301 リダイレクトするトイ実装。
//! struct TrailingSlashRedirect;
//!
//! impl Interceptor for TrailingSlashRedirect {
//! fn name(&self) -> &'static str {
//! "trailing-slash-redirect"
//! }
//!
//! fn intercept(&self, head: &RequestHead, _body: &[u8]) -> Option<Response> {
//! let path = head.path();
//! if path.len() > 1 && path.ends_with('/') {
//! let normalized = path.trim_end_matches('/').to_string();
//! return Response::redirect(301, normalized).ok();
//! }
//! None
//! }
//! }
//!
//! let interceptor = TrailingSlashRedirect;
//! let buf = b"GET /foo/ HTTP/1.1\r\n\r\n";
//! let head = match parse_request_head(buf).unwrap() {
//! ParseOutcome::Complete { head, .. } => head,
//! ParseOutcome::Incomplete => unreachable!(),
//! };
//! let response = interceptor.intercept(&head, b"").expect("should redirect");
//! assert_eq!(response.status, 301);
//! ```
//!
//! 404 応答の body をカスタムページへ差し替える `map_response` の例
//! (カスタムページは起動時にプリロードしておく契約):
//!
//! ```
//! use fandhe_backend_core::interceptor::Interceptor;
//! use fandhe_backend_http::request::{ParseOutcome, RequestHead, parse_request_head};
//! use fandhe_backend_http::response::Response;
//!
//! /// 404 応答の body だけを差し替えるトイ実装。ステータス・ヘッダは既存のまま。
//! struct Custom404 {
//! page: Vec<u8>,
//! }
//!
//! impl Interceptor for Custom404 {
//! fn name(&self) -> &'static str {
//! "custom-404"
//! }
//!
//! fn map_response(&self, _head: &RequestHead, response: Response) -> Response {
//! if response.status == 404 {
//! Response::new(404, self.page.clone())
//! } else {
//! response
//! }
//! }
//! }
//!
//! let interceptor = Custom404 { page: b"<html>not found</html>".to_vec() };
//! let buf = b"GET / HTTP/1.1\r\n\r\n";
//! let head = match parse_request_head(buf).unwrap() {
//! ParseOutcome::Complete { head, .. } => head,
//! ParseOutcome::Incomplete => unreachable!(),
//! };
//! let mapped = interceptor.map_response(&head, Response::empty(404));
//! assert_eq!(mapped.body, b"<html>not found</html>");
//! ```
use RequestHead;
use Response;
/// ユーザー向けのリクエストインターセプト・レスポンス改変拡張点。
///
/// 本モジュール doc の評価順序・fail-closed 除外・同期契約を必ず参照すること。
/// 既定実装は両フックとも no-op であり、片方のみをオーバーライドして使うことも
/// できる(`Middleware` の 2 フック 1 trait と同じ流儀)。