netget/server/http2/h2_server.rs
1//! HTTP/2 server implementation using h2 crate directly for full server push support
2
3use bytes::Bytes;
4use h2::server::{self, SendResponse};
5use http::{Request, Response, StatusCode};
6use std::collections::HashMap;
7use std::net::SocketAddr;
8use std::sync::Arc;
9use std::time::Duration;
10use tokio::sync::mpsc;
11use tracing::{debug, error, info, warn};
12
13use crate::llm::action_helper::call_llm;
14use crate::llm::actions::protocol_trait::ActionResult;
15use crate::llm::ollama_client::OllamaClient;
16use crate::protocol::Event;
17use crate::server::connection::ConnectionId;
18use crate::server::http_common::handler::{RequestData, RequestFilter};
19use crate::server::Http2Protocol;
20use crate::state::app_state::AppState;
21
22use super::actions::HTTP2_REQUEST_EVENT;
23use super::push::PendingPush;
24
25/// How long a peer that has connected and produced nothing may hold a slot.
26///
27/// HTTP/2 is client-speaks-first: the connection preface (and, over TLS, the ClientHello before
28/// it) is the first thing on the wire and the server says nothing before it, so a peer that has
29/// completed the TCP handshake and sent no byte has begun no connection at all. 30 seconds,
30/// the number every HTTP-shaped server here uses for this state, for `src/server/http/mod.rs`'s
31/// reasons. It is safe against NetGet's own HTTP/2 client because that client is **lazy**: it
32/// is a pooled `reqwest` client with `http2_prior_knowledge()` and opens no socket until it has
33/// a request to send, so nothing here is ever connected and waiting on a person. Declared as the
34/// `first_byte_timeout_secs` startup parameter, so an operator can change it.
35pub const FIRST_BYTE_READ_TIMEOUT: Duration = Duration::from_secs(30);
36
37/// How long the TLS handshake and the HTTP/2 preface exchange may take once the peer has sent
38/// its first byte.
39///
40/// This phase involves no model and no person — rustls and `h2` answer it by themselves — so
41/// there is nothing legitimate a peer can be waiting on, and a peer that sends one byte and then
42/// stalls would otherwise hold the slot for as long as it likes, having passed the first-byte
43/// bound. Thirty seconds, which is far longer than a real handshake on any network and the same
44/// number `tls`'s handshake wait uses.
45const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
46
47/// How long an established connection may carry no request at all before it is closed.
48///
49/// 300 seconds, chosen to stay above the 90 seconds `reqwest` keeps an idle pooled connection —
50/// NetGet's own HTTP/2 client is a pooled `reqwest` client, and a bound below the pool's races
51/// it: the client picks a connection the server is about to close and loses the request. nginx's
52/// `http2_idle_timeout` (180s) sits below this; being more generous costs one slot per idle
53/// client, and [`MAX_CONNECTIONS`] bounds the slots.
54///
55/// **A request still being answered is not silence.** Each stream's task holds the connection's
56/// [`ConnectionActivity`] busy for the whole of its answer — a model round-trip, or an event a
57/// `manual` rule parked for a human (`src/state/intercepts.rs`, 300s by default) — and the
58/// watchdog never fires while anything is in flight. HTTP/2 PING frames are answered by `h2`
59/// itself and are not counted as activity: a PING carries no request, and a client that only
60/// pings is holding a slot for nothing. Declared as the `idle_timeout_secs` startup parameter.
61///
62/// When it fires the connection is closed with a GOAWAY (`graceful_shutdown`), which every
63/// HTTP/2 client treats as "open a new connection for the next request", rather than with a bare
64/// FIN that a client could mistake for a crashed server.
65pub const IDLE_BETWEEN_REQUESTS_TIMEOUT: Duration = Duration::from_secs(300);
66
67/// How long the GOAWAY sent at the idle bound is given to reach the peer before the socket is
68/// dropped anyway. The connection is idle by definition, so there is nothing to drain; this only
69/// stops a peer that has stopped reading from holding the close open.
70const GOAWAY_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
71
72/// Concurrent connections this server admits.
73///
74/// 128, the same as HTTP/1.1 and for the same reason: every stream may buffer one request body
75/// of up to `http_common::MAX_REQUEST_BODY_BYTES` (8 MiB), which is the largest per-connection
76/// cost in NetGet's HTTP family, and the cap is what turns that per-connection cost into a total
77/// one. Not configurable: a bound decided by configuration is a bound an attacker can ask you
78/// to raise.
79const MAX_CONNECTIONS: usize = 128;
80
81/// What a cleartext (h2c) peer over [`MAX_CONNECTIONS`] is told before the socket closes.
82///
83/// HTTP/1.1 `503 Service Unavailable` with a `Retry-After`, deliberately in the older protocol,
84/// for the reason `src/server/etcd/mod.rs` gives: the refused peer has not sent the HTTP/2
85/// preface, so nothing has been negotiated, and an HTTP/2 GOAWAY would have to follow a SETTINGS
86/// frame this server is declining to exchange. Every HTTP client can read a 503.
87///
88/// **Over TLS the refusal is a plain close instead** (see `spawn_with_push_support`): plaintext
89/// bytes written to a peer that is about to send a ClientHello are a malformed TLS record, not a
90/// refusal — the same reasoning that makes `doh` and `dot` close silently.
91pub const CONNECTION_CAP_REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n\
92 Content-Length: 0\r\nRetry-After: 5\r\nConnection: close\r\n\r\n";
93
94/// `SETTINGS_MAX_CONCURRENT_STREAMS`: how many streams one peer may have open at once.
95///
96/// 100, the floor RFC 9113 §6.5.2 recommends so as "not to unnecessarily limit parallelism",
97/// and Apache's `H2MaxSessionStreams` default (nginx uses 128). `h2` advertises no limit at all
98/// unless told to, so without this one admitted connection could open streams without end, each
99/// with a task, a parked request and a body buffer. A stream past the limit is reset by `h2`
100/// with `REFUSED_STREAM`, which tells the client the request was not processed and is safe to
101/// retry; conforming clients never send one because they read this SETTINGS first.
102pub const MAX_CONCURRENT_STREAMS: u32 = 100;
103
104/// `SETTINGS_INITIAL_WINDOW_SIZE`: how much request body a stream may send before this server
105/// reads it. 65,535 bytes, the protocol's own default, stated rather than inherited: it bounds
106/// what `h2` holds for a stream this server has stopped reading (a refused body, see
107/// [`BodyBudget`]), and the handler releases capacity as it reads, so it costs an upload nothing
108/// but round trips.
109pub const INITIAL_STREAM_WINDOW_BYTES: u32 = 65_535;
110
111/// The connection-level receive window: the most unread request-body bytes `h2` buffers across
112/// every stream of one connection together. 1 MiB, so that [`MAX_CONCURRENT_STREAMS`] streams
113/// uploading at once do not serialise on the protocol's 64 KiB default while the total stays
114/// small next to [`CONNECTION_BODY_BUDGET_BYTES`].
115pub const INITIAL_CONNECTION_WINDOW_BYTES: u32 = 1024 * 1024;
116
117/// `SETTINGS_MAX_FRAME_SIZE`: the largest frame payload this server accepts. 16,384 bytes, the
118/// protocol minimum and default. Nothing a request carries needs a larger frame, and a larger
119/// frame is a larger single allocation a peer can ask for.
120pub const MAX_FRAME_BYTES: u32 = 16_384;
121
122/// `SETTINGS_MAX_HEADER_LIST_SIZE`: the largest decoded header list one request may carry.
123///
124/// 32 KiB, nginx's HTTP/1.1 equivalent (`large_client_header_buffers 4 8k`). `h2`'s own default
125/// is 16 MiB, which times [`MAX_CONCURRENT_STREAMS`] is 1.6 GiB of headers per connection; at
126/// 32 KiB it is 3.2 MiB. Real browsers send a few kilobytes including cookies.
127pub const MAX_HEADER_LIST_BYTES: u32 = 32 * 1024;
128
129/// The request-body bytes all streams of one connection may hold at once.
130///
131/// Each stream buffers its body whole before the model sees it, up to
132/// `http_common::MAX_REQUEST_BODY_BYTES` (8 MiB). Per stream alone, that is 800 MiB a
133/// connection at [`MAX_CONCURRENT_STREAMS`] and 100 GiB across [`MAX_CONNECTIONS`] — no bound
134/// at all in practice. So the streams of a connection share one budget of the same 8 MiB: a
135/// single upload can still use all of it, and the per-connection ceiling is what HTTP/1.1's
136/// one-request-at-a-time connection costs. A stream whose body would take the connection past
137/// it is answered `503` with `Retry-After` (`decision=refused_connection_body_budget`) and never
138/// reaches the model; its bytes are released as soon as it is refused, and every stream's are
139/// released when its answer has been sent.
140///
141/// Per connection that makes body buffers 8 MiB, `h2`'s unread data at most
142/// [`INITIAL_CONNECTION_WINDOW_BYTES`] (1 MiB), and decoded headers at most
143/// [`MAX_CONCURRENT_STREAMS`] × [`MAX_HEADER_LIST_BYTES`] (3.2 MiB): about 12 MiB, and about
144/// 1.5 GiB across [`MAX_CONNECTIONS`].
145pub const CONNECTION_BODY_BUDGET_BYTES: usize = crate::server::http_common::MAX_REQUEST_BODY_BYTES;
146
147/// The `h2` server builder every HTTP/2 connection is handshaken with — prior-knowledge h2c,
148/// TLS, and the HTTP/1.1 `Upgrade: h2c` path in `src/server/http/mod.rs` alike — so the
149/// SETTINGS above hold on every way in.
150pub fn bounded_h2_builder() -> server::Builder {
151 let mut builder = server::Builder::new();
152 builder
153 .max_concurrent_streams(MAX_CONCURRENT_STREAMS)
154 .initial_window_size(INITIAL_STREAM_WINDOW_BYTES)
155 .initial_connection_window_size(INITIAL_CONNECTION_WINDOW_BYTES)
156 .max_frame_size(MAX_FRAME_BYTES)
157 .max_header_list_size(MAX_HEADER_LIST_BYTES);
158 builder
159}
160
161/// One connection's share of [`CONNECTION_BODY_BUDGET_BYTES`], cloned into each stream's task.
162#[derive(Clone, Default)]
163pub struct BodyBudget(Arc<std::sync::atomic::AtomicUsize>);
164
165impl BodyBudget {
166 pub fn new() -> Self {
167 Self::default()
168 }
169
170 /// Bytes currently held by this connection's streams.
171 pub fn in_use(&self) -> usize {
172 self.0.load(std::sync::atomic::Ordering::SeqCst)
173 }
174
175 fn reservation(&self) -> BodyReservation {
176 BodyReservation {
177 budget: self.clone(),
178 held: 0,
179 }
180 }
181}
182
183/// The bytes one stream's body holds against its connection's [`BodyBudget`]; released on drop.
184struct BodyReservation {
185 budget: BodyBudget,
186 held: usize,
187}
188
189impl BodyReservation {
190 /// Take `n` more bytes, or refuse without taking any if the connection would exceed its
191 /// budget.
192 fn grow(&mut self, n: usize) -> bool {
193 use std::sync::atomic::Ordering;
194 let taken = self
195 .budget
196 .0
197 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |used| {
198 used.checked_add(n)
199 .filter(|&total| total <= CONNECTION_BODY_BUDGET_BYTES)
200 })
201 .is_ok();
202 if taken {
203 self.held += n;
204 }
205 taken
206 }
207}
208
209impl Drop for BodyReservation {
210 fn drop(&mut self) {
211 self.budget
212 .0
213 .fetch_sub(self.held, std::sync::atomic::Ordering::SeqCst);
214 }
215}
216
217/// The two read bounds a connection is served under, resolved from the startup parameters.
218#[derive(Clone, Copy, Debug)]
219pub struct H2Bounds {
220 pub first_byte: Duration,
221 pub idle: Duration,
222}
223
224impl H2Bounds {
225 /// The declared startup parameters, falling back to the documented defaults.
226 pub fn from_secs(first_byte_secs: Option<u64>, idle_secs: Option<u64>) -> Self {
227 Self {
228 first_byte: first_byte_secs
229 .map(Duration::from_secs)
230 .unwrap_or(FIRST_BYTE_READ_TIMEOUT),
231 idle: idle_secs
232 .map(Duration::from_secs)
233 .unwrap_or(IDLE_BETWEEN_REQUESTS_TIMEOUT),
234 }
235 }
236}
237
238/// HTTP/2 server with full server push support
239pub struct H2Server;
240
241impl H2Server {
242 /// Spawn HTTP/2 server using h2 crate directly
243 pub async fn spawn_with_push_support(
244 listen_addr: SocketAddr,
245 llm_client: OllamaClient,
246 app_state: Arc<AppState>,
247 status_tx: mpsc::UnboundedSender<String>,
248 server_id: crate::state::ServerId,
249 tls_config: Option<Arc<rustls::ServerConfig>>,
250 bounds: H2Bounds,
251 ) -> anyhow::Result<SocketAddr> {
252 // Same reuse semantics as the HTTP/1.1 listener, so a restart on the
253 // same port does not fail with EADDRINUSE while the old socket lingers.
254 let listener =
255 crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
256 let local_addr = listener.local_addr()?;
257
258 let protocol_name = if tls_config.is_some() {
259 "HTTP/2 (TLS, h2 with push)"
260 } else {
261 "HTTP/2 (h2c with push)"
262 };
263 info!("{} server listening on {}", protocol_name, local_addr);
264 let _ = status_tx.send(format!(
265 "[INFO] {} server listening on {}",
266 protocol_name, local_addr
267 ));
268
269 let protocol = Arc::new(Http2Protocol::new());
270
271 // Build the per-server request filter once, up front. This is the code
272 // path `Http2Protocol::spawn()` actually uses, so without this the
273 // `request_filter` startup param would be silently ignored for HTTP/2.
274 let filter = Arc::new(RequestFilter::from_startup_params(
275 app_state
276 .get_server(server_id)
277 .await
278 .and_then(|s| s.startup_params)
279 .as_ref(),
280 ));
281 // Fail-open parsing: surface any dropped rule loudly, since the effect
282 // is that more requests reach the LLM, not fewer.
283 for warning in filter.warnings() {
284 let _ = status_tx.send(format!("[ERROR] HTTP/2 request_filter: {}", warning));
285 }
286
287 // Create TLS acceptor if TLS is enabled
288 let tls_acceptor = tls_config.map(|config| tokio_rustls::TlsAcceptor::from(config));
289
290 // Over TLS a plaintext 503 would be a malformed record, so the refusal is a plain close
291 // (see CONNECTION_CAP_REFUSAL).
292 let refusal: &'static [u8] = if tls_acceptor.is_some() {
293 b""
294 } else {
295 CONNECTION_CAP_REFUSAL
296 };
297 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
298
299 // Spawn server loop
300 let task_registrar = app_state.clone();
301 let accept_handle = tokio::spawn(async move {
302 loop {
303 match crate::server::accept_bounded::accept_bounded(
304 &listener,
305 &limiter,
306 refusal,
307 "HTTP/2",
308 Some(&status_tx),
309 )
310 .await
311 {
312 Ok((tcp_stream, remote_addr, permit)) => {
313 let connection_id =
314 ConnectionId::new(app_state.get_next_unified_id().await);
315 let local_addr_conn = tcp_stream.local_addr().unwrap_or(local_addr);
316 info!(
317 "Accepted {} connection {} from {}",
318 protocol_name, connection_id, remote_addr
319 );
320
321 // Add connection to ServerInstance
322 use crate::state::server::{
323 ConnectionState as ServerConnectionState, ConnectionStatus,
324 ProtocolConnectionInfo,
325 };
326 let now = crate::utils::clock::Instant::now();
327 let conn_state = ServerConnectionState {
328 id: connection_id,
329 remote_addr,
330 local_addr: local_addr_conn,
331 bytes_sent: 0,
332 bytes_received: 0,
333 packets_sent: 0,
334 packets_received: 0,
335 last_activity: now,
336 status: ConnectionStatus::Active,
337 status_changed_at: now,
338 protocol_info: ProtocolConnectionInfo::new(serde_json::json!({
339 "recent_requests": []
340 })),
341 };
342 app_state
343 .add_connection_to_server(server_id, conn_state)
344 .await;
345 let _ = status_tx.send("__UPDATE_UI__".to_string());
346
347 let llm_client_clone = llm_client.clone();
348 let app_state_clone = app_state.clone();
349 let status_tx_clone = status_tx.clone();
350 let protocol_clone = protocol.clone();
351 let tls_acceptor_clone = tls_acceptor.clone();
352 let filter_clone = filter.clone();
353
354 // Tracked, not detached: stop_server must abort this connection too.
355 // A detached one kept reading, answering and calling the model on a
356 // server the operator had stopped.
357 let task_owner = app_state.clone();
358 task_owner
359 .spawn_server_task(server_id, async move {
360 // Held for the life of the connection, so the cap counts live
361 // connections rather than accepts.
362 let _permit = permit;
363
364 // First-byte bound, before rustls or h2 sees the socket. `peek`
365 // waits for data without consuming it, so the ClientHello or the
366 // connection preface is still there for them afterwards.
367 match tokio::time::timeout(
368 bounds.first_byte,
369 tcp_stream.peek(&mut [0u8; 1]),
370 )
371 .await
372 {
373 Ok(Ok(0)) | Ok(Err(_)) => {
374 app_state_clone
375 .close_connection_on_server(server_id, connection_id)
376 .await;
377 let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
378 return;
379 }
380 Ok(Ok(_)) => {}
381 Err(_) => {
382 debug!(
383 "{} connection {} sent nothing for {}s; closing \
384 before the preface",
385 protocol_name,
386 connection_id,
387 bounds.first_byte.as_secs()
388 );
389 app_state_clone
390 .close_connection_on_server(server_id, connection_id)
391 .await;
392 let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
393 return;
394 }
395 }
396
397 // Perform TLS handshake if TLS is enabled
398 let result: Result<(), Box<dyn std::error::Error + Send + Sync>> =
399 if let Some(acceptor) = tls_acceptor_clone {
400 match tokio::time::timeout(
401 HANDSHAKE_TIMEOUT,
402 acceptor.accept(tcp_stream),
403 )
404 .await
405 .unwrap_or_else(|_| {
406 Err(std::io::Error::new(
407 std::io::ErrorKind::TimedOut,
408 "TLS handshake did not complete in time",
409 ))
410 }) {
411 Ok(tls_stream) => {
412 debug!(
413 "{} TLS handshake complete with {}",
414 protocol_name, remote_addr
415 );
416 let _ = status_tx_clone.send(format!(
417 "[DEBUG] {} TLS handshake complete with {}",
418 protocol_name, remote_addr
419 ));
420 handle_h2_connection(
421 tls_stream,
422 connection_id,
423 server_id,
424 llm_client_clone,
425 app_state_clone.clone(),
426 status_tx_clone.clone(),
427 protocol_clone,
428 filter_clone,
429 bounds.idle,
430 )
431 .await
432 }
433 Err(e) => {
434 error!(
435 "{} TLS handshake failed: {}",
436 protocol_name, e
437 );
438 let _ = status_tx_clone.send(format!(
439 "[ERROR] {} TLS handshake failed: {}",
440 protocol_name, e
441 ));
442 Err(Box::new(e))
443 }
444 }
445 } else {
446 // No TLS, use plain TCP (h2c)
447 handle_h2_connection(
448 tcp_stream,
449 connection_id,
450 server_id,
451 llm_client_clone,
452 app_state_clone.clone(),
453 status_tx_clone.clone(),
454 protocol_clone,
455 filter_clone,
456 bounds.idle,
457 )
458 .await
459 };
460
461 if let Err(e) = result {
462 error!("{} connection error: {}", protocol_name, e);
463 }
464
465 // Mark connection as closed
466 app_state_clone
467 .close_connection_on_server(server_id, connection_id)
468 .await;
469 let _ = status_tx_clone.send(format!(
470 "✗ {} connection {connection_id} closed",
471 protocol_name
472 ));
473 let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
474 })
475 .await;
476 }
477 Err(e) => {
478 error!("Failed to accept HTTP/2 connection: {}", e);
479 break;
480 }
481 }
482 }
483 });
484
485 // Without this, stop_server cannot cancel the accept loop and the socket
486 // stays bound after the server is removed.
487 task_registrar
488 .register_server_task(server_id, accept_handle)
489 .await;
490
491 Ok(local_addr)
492 }
493}
494
495/// Handle a single HTTP/2 connection with full server push support
496#[allow(clippy::too_many_arguments)]
497async fn handle_h2_connection<T>(
498 tcp_stream: T,
499 connection_id: ConnectionId,
500 server_id: crate::state::ServerId,
501 llm_client: OllamaClient,
502 app_state: Arc<AppState>,
503 status_tx: mpsc::UnboundedSender<String>,
504 protocol: Arc<Http2Protocol>,
505 filter: Arc<RequestFilter>,
506 idle_timeout: Duration,
507) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
508where
509 T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
510{
511 // The preface exchange is bounded separately from the first byte: a peer that sent one byte
512 // and stalled has passed the first-byte bound and would otherwise sit here forever.
513 let mut h2_conn = match tokio::time::timeout(
514 HANDSHAKE_TIMEOUT,
515 bounded_h2_builder().handshake(tcp_stream),
516 )
517 .await
518 {
519 Ok(conn) => conn?,
520 Err(_) => {
521 debug!(
522 "HTTP/2 connection {} did not complete the preface in {}s; closing",
523 connection_id,
524 HANDSHAKE_TIMEOUT.as_secs()
525 );
526 return Ok(());
527 }
528 };
529 debug!("HTTP/2 handshake complete for connection {}", connection_id);
530
531 // Whether this connection is answering anything. Every stream's task holds it busy for the
532 // whole of its answer, so the idle watchdog below cannot close a connection whose request is
533 // waiting on the model or parked for a human, however long that takes.
534 let activity = Arc::new(crate::server::accept_bounded::ConnectionActivity::new());
535 let body_budget = BodyBudget::new();
536
537 // Handle incoming requests. `accept` must keep being polled while streams are answered —
538 // it is what drives the connection, including the frames those answers write — so the
539 // deadline is a watchdog racing it rather than a timeout around it.
540 loop {
541 let next = tokio::select! {
542 next = h2_conn.accept() => next,
543 _ = crate::server::accept_bounded::watch_idle(Arc::clone(&activity), idle_timeout) => {
544 debug!(
545 "HTTP/2 connection {} carried no request for {}s; sending GOAWAY",
546 connection_id,
547 idle_timeout.as_secs()
548 );
549 h2_conn.graceful_shutdown();
550 let _ = tokio::time::timeout(GOAWAY_DRAIN_TIMEOUT, async {
551 while let Some(_stream) = h2_conn.accept().await {}
552 })
553 .await;
554 return Ok(());
555 }
556 };
557 let Some(result) = next else { break };
558 let (request, send_response) = result?;
559 activity.touch();
560
561 let llm_clone = llm_client.clone();
562 let app_state_clone = app_state.clone();
563 let status_clone = status_tx.clone();
564 let protocol_clone = protocol.clone();
565 let filter_clone = filter.clone();
566 let busy = activity.busy();
567 let stream_budget = body_budget.clone();
568
569 // One task per stream, tracked so stop_server aborts an answer in progress too.
570 let task_owner = app_state.clone();
571 task_owner
572 .spawn_server_task(server_id, async move {
573 let _busy = busy;
574 if let Err(e) = handle_h2_request(
575 request,
576 send_response,
577 connection_id,
578 server_id,
579 llm_clone,
580 app_state_clone,
581 status_clone,
582 protocol_clone,
583 filter_clone,
584 stream_budget,
585 )
586 .await
587 {
588 error!("Error handling HTTP/2 request: {}", e);
589 }
590 })
591 .await;
592 }
593
594 Ok(())
595}
596
597/// Build an `http::Response<()>` from model- or config-supplied parts without
598/// panicking and without failing the whole request: an out-of-range status
599/// becomes 500 and headers hyper rejects (e.g. containing CR/LF) are dropped.
600fn build_h2_response_head(
601 status: u16,
602 headers: impl IntoIterator<Item = (String, String)>,
603 context: &str,
604) -> Response<()> {
605 let status_code = StatusCode::from_u16(status).unwrap_or_else(|_| {
606 error!(
607 "{}: invalid HTTP status {} (must be 100-599), sending 500 instead",
608 context, status
609 );
610 StatusCode::INTERNAL_SERVER_ERROR
611 });
612
613 let mut builder = Response::builder().status(status_code);
614 for (name, value) in headers {
615 match (
616 http::header::HeaderName::from_bytes(name.as_bytes()),
617 http::header::HeaderValue::from_str(&value),
618 ) {
619 (Ok(n), Ok(v)) => builder = builder.header(n, v),
620 _ => warn!("{}: dropping invalid response header {:?}", context, name),
621 }
622 }
623
624 builder.body(()).unwrap_or_else(|e| {
625 error!(
626 "{}: failed to build response ({}), sending bare 500",
627 context, e
628 );
629 let mut fallback = Response::new(());
630 *fallback.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
631 fallback
632 })
633}
634
635/// Handle a single HTTP/2 request with server push support
636#[allow(clippy::too_many_arguments)]
637pub async fn handle_h2_request(
638 request: Request<h2::RecvStream>,
639 mut send_response: SendResponse<Bytes>,
640 connection_id: ConnectionId,
641 server_id: crate::state::ServerId,
642 llm_client: OllamaClient,
643 app_state: Arc<AppState>,
644 status_tx: mpsc::UnboundedSender<String>,
645 protocol: Arc<Http2Protocol>,
646 filter: Arc<RequestFilter>,
647 body_budget: BodyBudget,
648) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
649 // Extract request metadata
650 let method = request.method().to_string();
651 // For HTTP/2, only use the path+query portion (not scheme/host)
652 let uri = request
653 .uri()
654 .path_and_query()
655 .map(|pq| pq.as_str())
656 .unwrap_or(request.uri().path())
657 .to_string();
658 let version = format!("{:?}", request.version());
659
660 // Extract headers
661 let mut headers = HashMap::new();
662 for (name, value) in request.headers() {
663 if let Ok(value_str) = value.to_str() {
664 headers.insert(name.to_string(), value_str.to_string());
665 }
666 }
667
668 // Read request body from h2::RecvStream, bounded by the same cap HTTP/1.1 uses.
669 // `release_capacity` re-opens the flow-control window after every chunk, so without
670 // a total limit a peer can stream an unbounded amount into this Vec — and the body
671 // ends up in an LLM prompt, where anything past a few kilobytes is cost with no
672 // benefit. Over the cap the stream is answered 413 and never reaches the model.
673 let mut body_stream = request.into_body();
674 let mut body_bytes = Vec::new();
675 let mut body_too_large = false;
676 // Held until this stream's answer has been sent; see CONNECTION_BODY_BUDGET_BYTES.
677 let mut reservation = body_budget.reservation();
678 let mut over_connection_budget = false;
679
680 loop {
681 match body_stream.data().await {
682 Some(Ok(chunk)) => {
683 if body_bytes.len() + chunk.len()
684 > crate::server::http_common::MAX_REQUEST_BODY_BYTES
685 {
686 body_too_large = true;
687 break;
688 }
689 if !reservation.grow(chunk.len()) {
690 over_connection_budget = true;
691 break;
692 }
693 body_bytes.extend_from_slice(&chunk);
694 // Release flow control capacity for this chunk
695 let _ = body_stream.flow_control().release_capacity(chunk.len());
696 }
697 Some(Err(e)) => {
698 warn!("Error reading request body: {}", e);
699 let _ = status_tx.send(format!("[WARN] Error reading body: {}", e));
700 break;
701 }
702 None => {
703 // End of stream
704 break;
705 }
706 }
707 }
708
709 if over_connection_budget {
710 let held_by_others = body_budget.in_use().saturating_sub(body_bytes.len());
711 // Release this stream's share before answering: it is refused, not waiting.
712 drop(body_bytes);
713 drop(reservation);
714 warn!(
715 "HTTP/2 {} {} decision=refused_connection_body_budget: the connection's other \
716 streams hold {} of {} body bytes",
717 method, uri, held_by_others, CONNECTION_BODY_BUDGET_BYTES
718 );
719 let _ = status_tx.send(format!("→ HTTP/2 {} {} → 503", method, uri));
720 let body = "Service Unavailable: this connection is already buffering as many request \
721 bodies as it may; retry when an earlier request has been answered\n";
722 let response = build_h2_response_head(
723 503,
724 [
725 ("content-type".to_string(), "text/plain".to_string()),
726 ("retry-after".to_string(), "1".to_string()),
727 ],
728 "HTTP/2 connection body budget",
729 );
730 let mut stream = send_response.send_response(response, false)?;
731 stream.send_data(Bytes::from(body), true)?;
732 app_state
733 .update_connection_stats(
734 server_id,
735 connection_id,
736 None,
737 Some(body.len() as u64),
738 None,
739 Some(1),
740 )
741 .await;
742 return Ok(());
743 }
744
745 if body_too_large {
746 warn!(
747 "HTTP/2 {} {} decision=refused_body_too_large: over {} bytes",
748 method,
749 uri,
750 crate::server::http_common::MAX_REQUEST_BODY_BYTES
751 );
752 let _ = status_tx.send(format!("→ HTTP/2 {} {} → 413", method, uri));
753 let body = format!(
754 "Payload Too Large: request bodies are limited to {} bytes\n",
755 crate::server::http_common::MAX_REQUEST_BODY_BYTES
756 );
757 let body_len = body.len() as u64;
758 let response = build_h2_response_head(
759 413,
760 [("content-type".to_string(), "text/plain".to_string())],
761 "HTTP/2 payload too large",
762 );
763 let mut stream = send_response.send_response(response, false)?;
764 stream.send_data(Bytes::from(body), true)?;
765 app_state
766 .update_connection_stats(
767 server_id,
768 connection_id,
769 None,
770 Some(body_len),
771 None,
772 Some(1),
773 )
774 .await;
775 return Ok(());
776 }
777
778 // Record the inbound message against the *connection* (streams are not in the
779 // connection map). This has to happen before the filter check so a filtered
780 // request still refreshes last_activity: cleanup_old_connections evicts any
781 // connection idle for 10s, and an HTTP/2 connection normally outlives that.
782 // A "packet" is one HTTP/2 message; bytes are body only, since h2 has already
783 // consumed the HEADERS frame by this point.
784 app_state
785 .update_connection_stats(
786 server_id,
787 connection_id,
788 Some(body_bytes.len() as u64),
789 None,
790 Some(1),
791 None,
792 )
793 .await;
794
795 // Log request
796 debug!(
797 "HTTP/2 request: {} {} {} ({} bytes) from {:?}",
798 method,
799 uri,
800 version,
801 body_bytes.len(),
802 connection_id
803 );
804 let _ = status_tx.send(format!(
805 "[DEBUG] HTTP/2 request: {} {} {} ({} bytes)",
806 method,
807 uri,
808 version,
809 body_bytes.len()
810 ));
811
812 // Apply the per-server request filter before spending an LLM call: only
813 // allowlisted requests reach the model, everything else gets the configured
814 // auto-response (default 404). Same semantics as HTTP/1.1.
815 let path = uri.split('?').next().unwrap_or(&uri).to_string();
816 if !filter.is_pass_through() {
817 let request_data = RequestData {
818 method: method.clone(),
819 uri: uri.clone(),
820 version: version.clone(),
821 headers: headers.clone(),
822 body_bytes: Bytes::from(body_bytes.clone()),
823 };
824 if !filter.allows(&request_data, &path) {
825 let (status, hdrs, body) = filter.rejection_parts();
826 let _ = status_tx.send(format!(
827 "↩ HTTP/2 filtered {} {} → {} (no LLM call)",
828 method, path, status
829 ));
830 let body_len = body.len() as u64;
831 let response = build_h2_response_head(status, hdrs, "HTTP/2 filtered_response");
832 let mut stream = send_response.send_response(response, false)?;
833 stream.send_data(Bytes::from(body), true)?;
834 app_state
835 .update_connection_stats(
836 server_id,
837 connection_id,
838 None,
839 Some(body_len),
840 None,
841 Some(1),
842 )
843 .await;
844 return Ok(());
845 }
846 }
847
848 // Create event for LLM.
849 //
850 // Request bodies are attacker-controlled and need not be UTF-8. Action/event design
851 // rules forbid handing the model raw bytes or base64, so the body is always presented
852 // as (lossily) decoded text — but a non-UTF-8 body is flagged explicitly rather than
853 // silently mangled into U+FFFD, so the model knows the text it sees is not the real
854 // payload. Same contract as HTTP/1.1.
855 let body_is_binary = std::str::from_utf8(&body_bytes).is_err();
856 let body_text = String::from_utf8_lossy(&body_bytes);
857 let mut event_data = serde_json::json!({
858 "method": method,
859 "uri": uri,
860 "version": version,
861 "headers": headers,
862 "body": if body_text.is_empty() { "" } else { body_text.as_ref() },
863 "body_bytes": body_bytes.len()
864 });
865 if body_is_binary {
866 event_data["body_is_binary"] = serde_json::Value::Bool(true);
867 }
868 let event = Event::new(&HTTP2_REQUEST_EVENT, event_data);
869
870 // Call LLM to generate response
871 match call_llm(
872 &llm_client,
873 &app_state,
874 server_id,
875 Some(connection_id),
876 &event,
877 protocol.as_ref(),
878 )
879 .await
880 {
881 Ok(execution_result) => {
882 debug!("LLM HTTP/2 response received");
883
884 // Display messages
885 for msg in execution_result.messages {
886 let _ = status_tx.send(msg);
887 }
888
889 // Extract response and pushes from protocol results
890 let mut status_code = 200;
891 let mut response_headers = HashMap::new();
892 let mut response_body = String::new();
893 let mut pushes = Vec::new();
894 // Did any action actually yield a response? Without this the 200 above is emitted
895 // whenever the model answered with nothing, with output that is not JSON, or with
896 // JSON carrying no status/headers/body — an empty 200 that a client reads as a
897 // successful, empty resource. A server push alone does not count: a PUSH_PROMISE
898 // is an extra resource offered alongside an answer, not the answer itself.
899 let mut produced_response = false;
900
901 for protocol_result in execution_result.protocol_results {
902 match protocol_result {
903 ActionResult::Output(output_data) => {
904 // Parse JSON output
905 if let Ok(json_value) =
906 serde_json::from_slice::<serde_json::Value>(&output_data)
907 {
908 // Check if this is a push directive
909 if json_value
910 .get("_push_directive")
911 .and_then(|v| v.as_bool())
912 .unwrap_or(false)
913 {
914 // This is a push request
915 let push = PendingPush {
916 path: json_value
917 .get("path")
918 .and_then(|v| v.as_str())
919 .unwrap_or("/")
920 .to_string(),
921 method: json_value
922 .get("method")
923 .and_then(|v| v.as_str())
924 .unwrap_or("GET")
925 .to_string(),
926 status: json_value
927 .get("status")
928 .and_then(|v| v.as_u64())
929 .unwrap_or(200)
930 as u16,
931 headers: json_value
932 .get("headers")
933 .and_then(|v| v.as_object())
934 .map(|obj| {
935 obj.iter()
936 .filter_map(|(k, v)| {
937 v.as_str().map(|s| (k.clone(), s.to_string()))
938 })
939 .collect()
940 })
941 .unwrap_or_default(),
942 body: json_value
943 .get("body")
944 .and_then(|v| v.as_str())
945 .unwrap_or("")
946 .as_bytes()
947 .to_vec(),
948 };
949 pushes.push(push);
950 } else {
951 // This is the main HTTP response
952 if let Some(status) =
953 json_value.get("status").and_then(|v| v.as_u64())
954 {
955 status_code = status as u16;
956 produced_response = true;
957 }
958 if let Some(headers_obj) =
959 json_value.get("headers").and_then(|v| v.as_object())
960 {
961 for (k, v) in headers_obj {
962 if let Some(v_str) = v.as_str() {
963 response_headers.insert(k.clone(), v_str.to_string());
964 produced_response = true;
965 }
966 }
967 }
968 if let Some(body) = json_value.get("body").and_then(|v| v.as_str())
969 {
970 response_body = body.to_string();
971 produced_response = true;
972 }
973 }
974 }
975 }
976 _ => {}
977 }
978 }
979
980 // Execute server pushes BEFORE sending main response
981 for push in pushes {
982 debug!("Executing server push for {}", push.path);
983 let push_body_len = push.body.len();
984
985 // Create push promise request
986 let mut push_request = http::Request::builder()
987 .method(push.method.as_str())
988 .uri(&push.path);
989
990 for (name, value) in &push.headers {
991 push_request = push_request.header(name, value);
992 }
993
994 if let Ok(push_req) = push_request.body(()) {
995 // Send push promise
996 match send_response.push_request(push_req) {
997 Ok(mut push_stream) => {
998 // Send push response
999 let mut push_response = http::Response::builder().status(push.status);
1000
1001 for (name, value) in &push.headers {
1002 push_response = push_response.header(name, value);
1003 }
1004
1005 if let Ok(push_resp) = push_response.body(()) {
1006 match push_stream.send_response(push_resp, false) {
1007 Ok(mut stream) => {
1008 if let Err(e) =
1009 stream.send_data(Bytes::from(push.body), true)
1010 {
1011 warn!(
1012 "Failed to send push body for {}: {}",
1013 push.path, e
1014 );
1015 } else {
1016 debug!("Successfully pushed {}", push.path);
1017 let _ = status_tx.send(format!(
1018 "⬆ Pushed {} ({} bytes)",
1019 push.path, push_body_len
1020 ));
1021 // A push is an extra message on the wire.
1022 app_state
1023 .update_connection_stats(
1024 server_id,
1025 connection_id,
1026 None,
1027 Some(push_body_len as u64),
1028 None,
1029 Some(1),
1030 )
1031 .await;
1032 }
1033 }
1034 Err(e) => {
1035 warn!(
1036 "Failed to send push response for {}: {}",
1037 push.path, e
1038 );
1039 }
1040 }
1041 }
1042 }
1043 Err(e) => {
1044 warn!("Client rejected push for {}: {}", push.path, e);
1045 }
1046 }
1047 }
1048 }
1049
1050 // Nothing usable came back.
1051 //
1052 // If the operator configured a `default_response`, that is a deliberate
1053 // answer for exactly this case and it is used — the parameter is declared by
1054 // `request_handling_startup_parameters()`, which HTTP/2 advertises, and until
1055 // now HTTP/2 read it nowhere, so setting it changed nothing here.
1056 //
1057 // With no `default_response`, refuse rather than emitting the pre-set 200,
1058 // which a client cannot tell from a real, empty answer.
1059 if !produced_response {
1060 if let Some((status, hdrs, body)) = filter.default_response_parts() {
1061 warn!(
1062 "HTTP/2 {} {} decision=default_response: actions ran but none \
1063 yielded a response; answering the configured default ({})",
1064 method, uri, status
1065 );
1066 let _ = status_tx.send(format!(
1067 "→ HTTP/2 {} {} → {} (configured default_response)",
1068 method, uri, status
1069 ));
1070 let body_len = body.len() as u64;
1071 let response = build_h2_response_head(status, hdrs, "HTTP/2 default_response");
1072 let mut stream = send_response.send_response(response, false)?;
1073 stream.send_data(Bytes::from(body), true)?;
1074 app_state
1075 .update_connection_stats(
1076 server_id,
1077 connection_id,
1078 None,
1079 Some(body_len),
1080 None,
1081 Some(1),
1082 )
1083 .await;
1084 return Ok(());
1085 }
1086
1087 warn!(
1088 "HTTP/2 {} {} decision=fail_closed_no_action: actions ran but none \
1089 yielded a response",
1090 method, uri
1091 );
1092 let _ = status_tx.send(format!(
1093 "✗ HTTP/2 {} {} → 500 (no response action produced)",
1094 method, uri
1095 ));
1096 let body = crate::utils::WireFailure::Unavailable.prefixed_text();
1097 let response = Response::builder()
1098 .status(StatusCode::INTERNAL_SERVER_ERROR)
1099 .body(())?;
1100 let mut stream = send_response.send_response(response, false)?;
1101 stream.send_data(Bytes::from(body), true)?;
1102 app_state
1103 .update_connection_stats(
1104 server_id,
1105 connection_id,
1106 None,
1107 Some(body.len() as u64),
1108 None,
1109 Some(1),
1110 )
1111 .await;
1112 return Ok(());
1113 }
1114
1115 // Send main response
1116 let _ = status_tx.send(format!(
1117 "→ HTTP/2 {} {} → {} ({} bytes)",
1118 method,
1119 uri,
1120 status_code,
1121 response_body.len()
1122 ));
1123
1124 // Status and headers come from model output; never let a bogus
1125 // value abort the request without a response.
1126 let body_len = response_body.len() as u64;
1127 let response = build_h2_response_head(status_code, response_headers, "HTTP/2");
1128 let mut stream = send_response.send_response(response, false)?;
1129 stream.send_data(Bytes::from(response_body), true)?;
1130 app_state
1131 .update_connection_stats(
1132 server_id,
1133 connection_id,
1134 None,
1135 Some(body_len),
1136 None,
1137 Some(1),
1138 )
1139 .await;
1140 }
1141 Err(e) => {
1142 // Same split HTTP/1.1 makes in `http_common::build_error_response`, which root
1143 // CLAUDE.md names as the shape to copy: an *overloaded* backend is transient
1144 // and gets 503 + `Retry-After` so the client backs off, anything else gets 500
1145 // and is treated as a permanent fault. HTTP/2 answered a flat 500 for both,
1146 // so a client could not tell "come back in a second" from "this is broken".
1147 //
1148 // Only the category reaches the peer; `e` is logged and never rendered onto
1149 // the wire.
1150 let failure = crate::utils::WireFailure::classify(&e);
1151 let (status, decision) = match failure {
1152 crate::utils::WireFailure::Overloaded => (
1153 StatusCode::SERVICE_UNAVAILABLE,
1154 "fail_closed_llm_overloaded",
1155 ),
1156 crate::utils::WireFailure::Unavailable => {
1157 (StatusCode::INTERNAL_SERVER_ERROR, "fail_closed_llm_error")
1158 }
1159 };
1160 error!(
1161 "HTTP/2 {} {} decision={} status={}: {}",
1162 method,
1163 uri,
1164 decision,
1165 status.as_u16(),
1166 e
1167 );
1168 let _ = status_tx.send(format!(
1169 "✗ LLM error for {} {} → {}: {}",
1170 method,
1171 uri,
1172 status.as_u16(),
1173 e
1174 ));
1175
1176 let error_body = failure.prefixed_text();
1177 let mut builder = Response::builder().status(status);
1178 if failure.is_overloaded() {
1179 builder = builder.header(http::header::RETRY_AFTER, "1");
1180 }
1181 let response = builder.body(())?;
1182 let mut stream = send_response.send_response(response, false)?;
1183 stream.send_data(Bytes::from(error_body), true)?;
1184 app_state
1185 .update_connection_stats(
1186 server_id,
1187 connection_id,
1188 None,
1189 Some(error_body.len() as u64),
1190 None,
1191 Some(1),
1192 )
1193 .await;
1194 }
1195 }
1196
1197 Ok(())
1198}