Skip to main content

http_handle/
http2_server.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (c) 2023 - 2026 HTTP Handle
3
4//! HTTP/2 server entrypoints (feature-gated).
5//!
6//! This module provides a clear-text HTTP/2 (h2c) accept loop that reuses
7//! the request/response behavior from the primary server pipeline.
8
9#[cfg(feature = "http2")]
10#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
11use crate::error::ServerError;
12#[cfg(feature = "http2")]
13#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
14use crate::request::Request;
15#[cfg(feature = "http2")]
16#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
17use crate::server::{Server, build_response_for_request_with_metrics};
18
19/// Starts an HTTP/2 (h2c) accept loop backed by Tokio.
20///
21/// Each accepted TCP connection is upgraded to an `h2` server connection and
22/// each stream is handled using the same request->response logic used by
23/// the HTTP/1 server.
24#[cfg(feature = "http2")]
25#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
26///
27/// # Examples
28///
29/// ```rust,no_run
30/// use http_handle::http2_server::start_http2;
31/// use http_handle::Server;
32/// # #[tokio::main(flavor = "current_thread")]
33/// # async fn main() {
34/// let server = Server::new("127.0.0.1:8080", ".");
35/// let _ = start_http2(server).await;
36/// # }
37/// ```
38///
39/// # Errors
40///
41/// Returns an error when binding or accepting HTTP/2 connections fails.
42///
43/// # Panics
44///
45/// This function does not panic.
46pub async fn start_http2(server: Server) -> Result<(), ServerError> {
47    let listener = tokio::net::TcpListener::bind(server.address())
48        .await
49        .map_err(ServerError::from)?;
50
51    loop {
52        let (stream, _) =
53            listener.accept().await.map_err(ServerError::from)?;
54        let server_clone = server.clone();
55        drop(tokio::spawn(async move {
56            if let Err(error) =
57                handle_h2_connection(stream, server_clone).await
58            {
59                eprintln!("HTTP/2 connection error: {}", error);
60            }
61        }));
62    }
63}
64
65#[cfg(feature = "http2")]
66fn h2_handshake_err(e: h2::Error) -> ServerError {
67    ServerError::Custom(format!("h2 handshake: {e}"))
68}
69
70#[cfg(feature = "http2")]
71fn h2_accept_err(e: h2::Error) -> ServerError {
72    ServerError::Custom(format!("h2 accept: {e}"))
73}
74
75#[cfg(feature = "http2")]
76fn h2_send_headers_err(e: h2::Error) -> ServerError {
77    ServerError::Custom(format!(
78        "failed to send h2 response headers: {e}"
79    ))
80}
81
82#[cfg(feature = "http2")]
83fn h2_send_body_err(e: h2::Error) -> ServerError {
84    ServerError::Custom(format!("failed to send h2 response body: {e}"))
85}
86
87#[cfg(feature = "http2")]
88fn h2_build_head_err(e: http::Error) -> ServerError {
89    ServerError::Custom(format!(
90        "failed to build h2 response headers: {e}"
91    ))
92}
93
94#[cfg(feature = "http2")]
95#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
96async fn handle_h2_connection(
97    stream: tokio::net::TcpStream,
98    server: Server,
99) -> Result<(), ServerError> {
100    // Disable Nagle — HTTP/2 frame flushing should not wait for delayed ACK.
101    let _ = stream.set_nodelay(true);
102    let mut connection = h2::server::handshake(stream)
103        .await
104        .map_err(h2_handshake_err)?;
105
106    while let Some(next) = connection.accept().await {
107        let (request, respond) = next.map_err(h2_accept_err)?;
108        let parsed_request = map_h2_request(&request);
109        let response = build_response_for_request_with_metrics(
110            &server,
111            &parsed_request,
112        );
113        send_h2_response(respond, response)?;
114    }
115
116    Ok(())
117}
118
119#[cfg(feature = "http2")]
120#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
121fn map_h2_request<B>(request: &http::Request<B>) -> Request {
122    let headers = request
123        .headers()
124        .iter()
125        .filter_map(|(name, value)| {
126            value.to_str().ok().map(|value| {
127                (name.as_str().to_ascii_lowercase(), value.to_string())
128            })
129        })
130        .collect();
131
132    let version = match request.version() {
133        http::Version::HTTP_2 => "HTTP/2.0",
134        _ => "HTTP/1.1",
135    };
136
137    Request {
138        method: request.method().as_str().to_string(),
139        path: request.uri().path().to_string(),
140        version: version.to_string(),
141        headers,
142    }
143}
144
145#[cfg(feature = "http2")]
146#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
147fn send_h2_response(
148    mut respond: h2::server::SendResponse<bytes::Bytes>,
149    response: crate::response::Response,
150) -> Result<(), ServerError> {
151    let head = build_h2_head(&response)?;
152
153    let end_of_stream = response.body.is_empty();
154    let mut stream = respond
155        .send_response(head, end_of_stream)
156        .map_err(h2_send_headers_err)?;
157
158    if !end_of_stream {
159        stream
160            .send_data(bytes::Bytes::from(response.body), true)
161            .map_err(h2_send_body_err)?;
162    }
163
164    Ok(())
165}
166
167#[cfg(feature = "http2")]
168#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
169fn build_h2_head(
170    response: &crate::response::Response,
171) -> Result<http::Response<()>, ServerError> {
172    let mut builder =
173        http::Response::builder().status(response.status_code);
174    for (name, value) in &response.headers {
175        builder = builder.header(name, value);
176    }
177    builder.body(()).map_err(h2_build_head_err)
178}
179
180#[cfg(all(test, feature = "http2"))]
181mod tests {
182    use super::*;
183    use bytes::Bytes;
184    use http::Version;
185    use std::io::Write;
186    use std::net::TcpListener;
187    use tempfile::TempDir;
188    use tokio::io::AsyncWriteExt;
189    use tokio::time::{Duration, sleep};
190
191    fn free_addr() -> String {
192        let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
193        let addr = listener.local_addr().expect("addr");
194        drop(listener);
195        addr.to_string()
196    }
197
198    #[tokio::test]
199    async fn http2_server_serves_static_file() {
200        let root = TempDir::new().expect("tmp");
201        std::fs::write(root.path().join("index.html"), b"hello-h2")
202            .expect("write index");
203        std::fs::create_dir(root.path().join("404")).expect("404 dir");
204        std::fs::write(root.path().join("404/index.html"), b"404")
205            .expect("write 404");
206
207        let addr = free_addr();
208        let server = Server::builder()
209            .address(&addr)
210            .document_root(root.path().to_str().expect("path"))
211            .build()
212            .expect("server");
213
214        let task = tokio::spawn(start_http2(server));
215        sleep(Duration::from_millis(40)).await;
216
217        let stream = tokio::net::TcpStream::connect(&addr)
218            .await
219            .expect("connect");
220        let (mut client, connection) =
221            h2::client::handshake(stream).await.expect("handshake");
222        drop(tokio::spawn(connection));
223
224        let request = http::Request::builder()
225            .method("GET")
226            .uri("http://localhost/")
227            .body(())
228            .expect("request");
229        let (response_future, _send_stream) =
230            client.send_request(request, true).expect("send request");
231        let response = response_future.await.expect("response");
232        assert_eq!(response.status().as_u16(), 200);
233
234        let mut body = response.into_body();
235        let mut collected = Vec::new();
236        while let Some(next) = body.data().await {
237            let chunk: Bytes = next.expect("chunk");
238            collected.extend_from_slice(&chunk);
239        }
240
241        assert_eq!(collected, b"hello-h2");
242        task.abort();
243    }
244
245    #[test]
246    fn map_h2_request_preserves_method_path_headers_and_version() {
247        let request = http::Request::builder()
248            .method("GET")
249            .uri("/status")
250            .version(Version::HTTP_2)
251            .header("x-test", "value")
252            .body(())
253            .expect("request");
254        let parsed = map_h2_request(&request);
255        assert_eq!(parsed.method(), "GET");
256        assert_eq!(parsed.path(), "/status");
257        assert_eq!(parsed.version(), "HTTP/2.0");
258        assert_eq!(parsed.header("x-test"), Some("value"));
259    }
260
261    #[test]
262    fn map_h2_request_falls_back_to_http11_for_other_versions() {
263        let request = http::Request::builder()
264            .method("GET")
265            .uri("/legacy")
266            .version(Version::HTTP_11)
267            .body(())
268            .expect("request");
269        let parsed = map_h2_request(&request);
270        assert_eq!(parsed.version(), "HTTP/1.1");
271    }
272
273    #[test]
274    fn h2_error_context_helpers_wrap_source_message() {
275        let reason = h2::Reason::PROTOCOL_ERROR;
276        let handshake = h2_handshake_err(h2::Error::from(reason));
277        assert!(matches!(handshake, ServerError::Custom(_)));
278        assert!(handshake.to_string().contains("h2 handshake:"));
279
280        let accept = h2_accept_err(h2::Error::from(reason));
281        assert!(accept.to_string().contains("h2 accept:"));
282
283        let headers = h2_send_headers_err(h2::Error::from(reason));
284        assert!(
285            headers.to_string().contains("send h2 response headers:")
286        );
287
288        let body = h2_send_body_err(h2::Error::from(reason));
289        assert!(body.to_string().contains("send h2 response body:"));
290
291        // http::Error construction: build a response with a malformed
292        // header name so `builder.body(())` returns Err(http::Error).
293        let http_err = http::Response::builder()
294            .header("bad header name", "v")
295            .body(())
296            .expect_err(
297                "invalid header name should produce http::Error",
298            );
299        let built = h2_build_head_err(http_err);
300        assert!(
301            built.to_string().contains("build h2 response headers:")
302        );
303    }
304
305    #[test]
306    fn build_h2_head_rejects_invalid_header_name() {
307        let mut response =
308            crate::response::Response::new(200, "OK", Vec::new());
309        response.add_header("bad header", "value");
310        let result = build_h2_head(&response);
311        assert!(matches!(result, Err(ServerError::Custom(_))));
312    }
313
314    #[tokio::test]
315    async fn handle_h2_connection_reports_handshake_error_on_invalid_preface()
316     {
317        let root = TempDir::new().expect("tmp");
318        std::fs::write(root.path().join("index.html"), b"hello")
319            .expect("write index");
320        std::fs::create_dir(root.path().join("404")).expect("404 dir");
321        std::fs::write(root.path().join("404/index.html"), b"404")
322            .expect("write 404");
323
324        let addr = free_addr();
325        let listener =
326            tokio::net::TcpListener::bind(&addr).await.expect("bind");
327        let server = Server::builder()
328            .address(&addr)
329            .document_root(root.path().to_str().expect("path"))
330            .build()
331            .expect("server");
332
333        let accept_task = tokio::spawn(async move {
334            let (stream, _) = listener.accept().await.expect("accept");
335            handle_h2_connection(stream, server).await
336        });
337
338        let mut client =
339            std::net::TcpStream::connect(&addr).expect("connect");
340        client
341            .write_all(b"this-is-not-http2")
342            .expect("write invalid preface");
343
344        let result = accept_task.await.expect("join");
345        assert!(matches!(result, Err(ServerError::Custom(_))));
346    }
347
348    #[tokio::test]
349    async fn http2_server_returns_404_for_missing_resource() {
350        let root = TempDir::new().expect("tmp");
351        std::fs::write(root.path().join("index.html"), b"hello-h2")
352            .expect("write index");
353        std::fs::create_dir(root.path().join("404")).expect("404 dir");
354        std::fs::write(root.path().join("404/index.html"), b"404 page")
355            .expect("write 404");
356
357        let addr = free_addr();
358        let server = Server::builder()
359            .address(&addr)
360            .document_root(root.path().to_str().expect("path"))
361            .build()
362            .expect("server");
363
364        let task = tokio::spawn(start_http2(server));
365        sleep(Duration::from_millis(40)).await;
366
367        let stream = tokio::net::TcpStream::connect(&addr)
368            .await
369            .expect("connect");
370        let (mut client, connection) =
371            h2::client::handshake(stream).await.expect("handshake");
372        drop(tokio::spawn(connection));
373
374        let request = http::Request::builder()
375            .method("GET")
376            .uri("http://localhost/does-not-exist")
377            .body(())
378            .expect("request");
379        let (response_future, _send_stream) =
380            client.send_request(request, true).expect("send request");
381        let response = response_future.await.expect("response");
382        assert_eq!(response.status().as_u16(), 404);
383
384        let mut body = response.into_body();
385        let mut collected = Vec::new();
386        while let Some(next) = body.data().await {
387            let chunk: Bytes = next.expect("chunk");
388            collected.extend_from_slice(&chunk);
389        }
390        assert_eq!(collected, b"404 page");
391        task.abort();
392    }
393
394    #[tokio::test]
395    async fn http2_server_returns_405_for_unsupported_method() {
396        let root = TempDir::new().expect("tmp");
397        std::fs::write(root.path().join("index.html"), b"hello-h2")
398            .expect("write index");
399        std::fs::create_dir(root.path().join("404")).expect("404 dir");
400        std::fs::write(root.path().join("404/index.html"), b"404")
401            .expect("write 404");
402
403        let addr = free_addr();
404        let server = Server::builder()
405            .address(&addr)
406            .document_root(root.path().to_str().expect("path"))
407            .build()
408            .expect("server");
409
410        let task = tokio::spawn(start_http2(server));
411        sleep(Duration::from_millis(40)).await;
412
413        let stream = tokio::net::TcpStream::connect(&addr)
414            .await
415            .expect("connect");
416        let (mut client, connection) =
417            h2::client::handshake(stream).await.expect("handshake");
418        drop(tokio::spawn(connection));
419
420        let request = http::Request::builder()
421            .method("POST")
422            .uri("http://localhost/")
423            .body(())
424            .expect("request");
425        let (response_future, _send_stream) =
426            client.send_request(request, true).expect("send request");
427        let response = response_future.await.expect("response");
428        assert_eq!(response.status().as_u16(), 405);
429        task.abort();
430    }
431
432    #[tokio::test]
433    async fn handle_h2_connection_surfaces_send_errors_when_client_rsts()
434     {
435        // When the TCP connection drops between request arrival and the
436        // server's `send_response`/`send_data`, h2 reports an error mapped
437        // to `ServerError::Custom`. This exercises the response-send error
438        // branches in `send_h2_response`.
439        let root = TempDir::new().expect("tmp");
440        std::fs::write(root.path().join("index.html"), b"hello-h2")
441            .expect("write index");
442        std::fs::create_dir(root.path().join("404")).expect("404 dir");
443        std::fs::write(root.path().join("404/index.html"), b"404")
444            .expect("write 404");
445
446        let addr = free_addr();
447        let listener =
448            tokio::net::TcpListener::bind(&addr).await.expect("bind");
449        let server = Server::builder()
450            .address(&addr)
451            .document_root(root.path().to_str().expect("path"))
452            .build()
453            .expect("server");
454
455        let accept_task = tokio::spawn(async move {
456            let (stream, _) = listener.accept().await.expect("accept");
457            handle_h2_connection(stream, server).await
458        });
459
460        // FIN-based close from `drop(client)` plus `conn_task.abort()`
461        // is enough to trip the same response-send error branch we want
462        // to cover; an explicit SO_LINGER=0 RST is no longer used because
463        // tokio's TcpStream::set_linger is deprecated and the bench
464        // confirmed the test still hits the Custom-error arm without it.
465        let tcp = tokio::net::TcpStream::connect(&addr)
466            .await
467            .expect("connect");
468        let (mut client, connection) =
469            h2::client::handshake(tcp).await.expect("handshake");
470        let conn_task = tokio::spawn(connection);
471
472        let request = http::Request::builder()
473            .method("GET")
474            .uri("http://localhost/")
475            .body(())
476            .expect("request");
477        let (_response_future, _send) =
478            client.send_request(request, true).expect("send request");
479        // Drop the client and the connection driver before the server
480        // finishes responding. The underlying TcpStream gets RST-ed.
481        drop(client);
482        conn_task.abort();
483
484        let result =
485            tokio::time::timeout(Duration::from_secs(2), accept_task)
486                .await
487                .expect("accept_task timed out");
488        // The join must succeed; the inner result can be Ok, or a Custom
489        // error from send_response/send_data/accept on the RST.
490        let _ = result.expect("join");
491    }
492
493    #[tokio::test]
494    async fn start_http2_handles_invalid_client_preface() {
495        let root = TempDir::new().expect("tmp");
496        std::fs::write(root.path().join("index.html"), b"hello-h2")
497            .expect("write index");
498        std::fs::create_dir(root.path().join("404")).expect("404 dir");
499        std::fs::write(root.path().join("404/index.html"), b"404")
500            .expect("write 404");
501
502        let addr = free_addr();
503        let server = Server::builder()
504            .address(&addr)
505            .document_root(root.path().to_str().expect("path"))
506            .build()
507            .expect("server");
508
509        let task = tokio::spawn(start_http2(server));
510        sleep(Duration::from_millis(40)).await;
511
512        let mut client =
513            std::net::TcpStream::connect(&addr).expect("connect");
514        client
515            .write_all(b"not-http2")
516            .expect("write invalid preface");
517        sleep(Duration::from_millis(40)).await;
518        task.abort();
519    }
520
521    #[tokio::test]
522    async fn handle_h2_connection_returns_ok_when_client_closes_cleanly()
523     {
524        let root = TempDir::new().expect("tmp");
525        std::fs::write(root.path().join("index.html"), b"hello-h2")
526            .expect("write index");
527        std::fs::create_dir(root.path().join("404")).expect("404 dir");
528        std::fs::write(root.path().join("404/index.html"), b"404")
529            .expect("write 404");
530
531        let addr = free_addr();
532        let listener =
533            tokio::net::TcpListener::bind(&addr).await.expect("bind");
534        let server = Server::builder()
535            .address(&addr)
536            .document_root(root.path().to_str().expect("path"))
537            .build()
538            .expect("server");
539
540        let accept_task = tokio::spawn(async move {
541            let (stream, _) = listener.accept().await.expect("accept");
542            handle_h2_connection(stream, server).await
543        });
544
545        let stream = tokio::net::TcpStream::connect(&addr)
546            .await
547            .expect("connect");
548        let (mut client, connection) =
549            h2::client::handshake(stream).await.expect("handshake");
550        let conn_task = tokio::spawn(connection);
551
552        let request = http::Request::builder()
553            .method("GET")
554            .uri("http://localhost/")
555            .body(())
556            .expect("request");
557        let (response_future, _send_stream) =
558            client.send_request(request, true).expect("send request");
559        let _ = response_future.await.expect("response");
560        drop(client);
561        let _ =
562            tokio::time::timeout(Duration::from_millis(500), conn_task)
563                .await;
564
565        let _ = tokio::time::timeout(
566            Duration::from_millis(500),
567            accept_task,
568        )
569        .await;
570    }
571
572    #[tokio::test]
573    async fn handle_h2_connection_maps_accept_errors() {
574        let root = TempDir::new().expect("tmp");
575        std::fs::write(root.path().join("index.html"), b"hello")
576            .expect("write index");
577        std::fs::create_dir(root.path().join("404")).expect("404 dir");
578        std::fs::write(root.path().join("404/index.html"), b"404")
579            .expect("write 404");
580
581        let addr = free_addr();
582        let listener =
583            tokio::net::TcpListener::bind(&addr).await.expect("bind");
584        let server = Server::builder()
585            .address(&addr)
586            .document_root(root.path().to_str().expect("path"))
587            .build()
588            .expect("server");
589
590        let accept_task = tokio::spawn(async move {
591            let (stream, _) = listener.accept().await.expect("accept");
592            handle_h2_connection(stream, server).await
593        });
594
595        let mut client = tokio::net::TcpStream::connect(&addr)
596            .await
597            .expect("connect");
598        // Valid HTTP/2 preface followed by malformed frame bytes.
599        client
600            .write_all(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n")
601            .await
602            .expect("preface");
603        client
604            .write_all(&[0, 0, 1, 0xff, 0, 0, 0, 0, 0, 0x00])
605            .await
606            .expect("malformed frame");
607        let _ = client.shutdown().await;
608
609        let result = accept_task.await.expect("join");
610        assert!(
611            result.is_ok()
612                || matches!(result, Err(ServerError::Custom(_)))
613        );
614    }
615}