Skip to main content

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}