scuffle_http/
lib.rs

1//! An HTTP server with support for HTTP/1, HTTP/2 and HTTP/3.
2//!
3//! It abstracts away [`hyper`](https://crates.io/crates/hyper) and [`h3`](https://crates.io/crates/h3) to provide a rather simple interface for creating and running a server that can handle all three protocols.
4//!
5//! See the [examples](./examples) directory for usage examples.
6#![cfg_attr(feature = "docs", doc = "\n\nSee the [changelog][changelog] for a full release history.")]
7#![cfg_attr(feature = "docs", doc = "## Feature flags")]
8#![cfg_attr(feature = "docs", doc = document_features::document_features!())]
9//! ## Why do we need this?
10//!
11//! This crate is designed to be a simple and easy to use HTTP server that supports HTTP/1, HTTP/2 and HTTP/3.
12//!
13//! Currently, there are simply no other crates that provide support for all three protocols with a unified API.
14//! This crate aims to fill that gap.
15//!
16//! ## Example
17//!
18//! The following example demonstrates how to create a simple HTTP server (without TLS) that responds with "Hello, world!" to all requests on port 3000.
19//!
20//! ```rust
21//! # use scuffle_future_ext::FutureExt;
22//! # tokio_test::block_on(async {
23//! # let run = async {
24//! let service = scuffle_http::service::fn_http_service(|req| async move {
25//!     scuffle_http::Response::builder()
26//!         .status(scuffle_http::http::StatusCode::OK)
27//!         .header(scuffle_http::http::header::CONTENT_TYPE, "text/plain")
28//!         .body("Hello, world!".to_string())
29//! });
30//! let service_factory = scuffle_http::service::service_clone_factory(service);
31//!
32//! scuffle_http::HttpServer::builder()
33//!     .service_factory(service_factory)
34//!     .bind("[::]:3000".parse().unwrap())
35//!     .build()
36//!     .run()
37//!     .await
38//!     .expect("server failed");
39//! # };
40//! # run.with_timeout(std::time::Duration::from_secs(1)).await.expect_err("test should have timed out");
41//! # });
42//! ```
43//!
44//! ### Missing Features
45//!
46//! - HTTP/3 webtransport support
47//! - Upgrading to websocket connections from HTTP/3 connections (this is usually done via HTTP/1.1 anyway)
48//!
49//! ## License
50//!
51//! This project is licensed under the MIT or Apache-2.0 license.
52//! You can choose between one of them if you use this work.
53//!
54//! `SPDX-License-Identifier: MIT OR Apache-2.0`
55#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))]
56#![cfg_attr(docsrs, feature(doc_auto_cfg))]
57#![deny(missing_docs)]
58#![deny(unsafe_code)]
59#![deny(unreachable_pub)]
60
61#[cfg(all(feature = "http3", not(feature = "tls-rustls")))]
62compile_error!("feature \"tls-rustls\" must be enabled when \"http3\" is enabled.");
63
64#[cfg(any(feature = "http1", feature = "http2", feature = "http3"))]
65pub mod backend;
66pub mod body;
67pub mod error;
68mod server;
69pub mod service;
70
71pub use http;
72pub use http::Response;
73pub use server::{HttpServer, HttpServerBuilder};
74
75/// An incoming request.
76pub type IncomingRequest = http::Request<body::IncomingBody>;
77
78/// Changelogs generated by [scuffle_changelog]
79#[cfg(feature = "docs")]
80#[scuffle_changelog::changelog]
81pub mod changelog {}
82
83#[cfg(test)]
84#[cfg_attr(all(test, coverage_nightly), coverage(off))]
85mod tests {
86    use std::convert::Infallible;
87    use std::path::PathBuf;
88    use std::time::Duration;
89
90    use scuffle_future_ext::FutureExt;
91
92    use crate::HttpServer;
93    use crate::service::{fn_http_service, service_clone_factory};
94
95    fn install_provider() {
96        #[cfg(feature = "tls-rustls")]
97        {
98            static ONCE: std::sync::Once = std::sync::Once::new();
99
100            ONCE.call_once(|| {
101                rustls::crypto::aws_lc_rs::default_provider()
102                    .install_default()
103                    .expect("failed to install aws lc provider");
104            });
105        }
106    }
107
108    fn get_available_addr() -> std::io::Result<std::net::SocketAddr> {
109        let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
110        listener.local_addr()
111    }
112
113    const RESPONSE_TEXT: &str = "Hello, world!";
114
115    #[allow(unused)]
116    fn file_path(item: &str) -> PathBuf {
117        if let Some(env) = std::env::var_os("ASSETS_DIR") {
118            PathBuf::from(env).join(item)
119        } else {
120            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("../../assets/{item}"))
121        }
122    }
123
124    #[allow(dead_code)]
125    async fn test_server<F, S>(builder: crate::HttpServerBuilder<F, S>, versions: &[reqwest::Version])
126    where
127        F: crate::service::HttpServiceFactory + std::fmt::Debug + Clone + Send + 'static,
128        F::Error: std::error::Error + Send,
129        F::Service: Clone + std::fmt::Debug + Send + 'static,
130        <F::Service as crate::service::HttpService>::Error: std::error::Error + Send + Sync,
131        <F::Service as crate::service::HttpService>::ResBody: Send,
132        <<F::Service as crate::service::HttpService>::ResBody as http_body::Body>::Data: Send,
133        <<F::Service as crate::service::HttpService>::ResBody as http_body::Body>::Error: std::error::Error + Send + Sync,
134        S: crate::server::http_server_builder::State,
135        S::ServiceFactory: crate::server::http_server_builder::IsSet,
136        S::Bind: crate::server::http_server_builder::IsUnset,
137        S::Ctx: crate::server::http_server_builder::IsUnset,
138    {
139        install_provider();
140
141        let addr = get_available_addr().expect("failed to get available address");
142        let (ctx, handler) = scuffle_context::Context::new();
143
144        let server = builder.bind(addr).ctx(ctx).build();
145
146        let handle = tokio::spawn(async move {
147            server.run().await.expect("server run failed");
148        });
149
150        // Wait for the server to start
151        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
152
153        let url = format!("http://{addr}/");
154
155        for version in versions {
156            let mut builder = reqwest::Client::builder().danger_accept_invalid_certs(true);
157
158            if *version == reqwest::Version::HTTP_3 {
159                builder = builder.http3_prior_knowledge();
160            } else if *version == reqwest::Version::HTTP_2 {
161                builder = builder.http2_prior_knowledge();
162            } else {
163                builder = builder.http1_only();
164            }
165
166            let client = builder.build().expect("failed to build client");
167
168            let request = client
169                .request(reqwest::Method::GET, &url)
170                .version(*version)
171                .body(RESPONSE_TEXT.to_string())
172                .build()
173                .expect("failed to build request");
174
175            let resp = client
176                .execute(request)
177                .await
178                .expect("failed to get response")
179                .text()
180                .await
181                .expect("failed to get text");
182
183            assert_eq!(resp, RESPONSE_TEXT);
184        }
185
186        handler.shutdown().await;
187        handle.await.expect("task failed");
188    }
189
190    #[cfg(feature = "tls-rustls")]
191    #[allow(dead_code)]
192    async fn test_tls_server<F, S>(builder: crate::HttpServerBuilder<F, S>, versions: &[reqwest::Version])
193    where
194        F: crate::service::HttpServiceFactory + std::fmt::Debug + Clone + Send + 'static,
195        F::Error: std::error::Error + Send,
196        F::Service: Clone + std::fmt::Debug + Send + 'static,
197        <F::Service as crate::service::HttpService>::Error: std::error::Error + Send + Sync,
198        <F::Service as crate::service::HttpService>::ResBody: Send,
199        <<F::Service as crate::service::HttpService>::ResBody as http_body::Body>::Data: Send,
200        <<F::Service as crate::service::HttpService>::ResBody as http_body::Body>::Error: std::error::Error + Send + Sync,
201        S: crate::server::http_server_builder::State,
202        S::ServiceFactory: crate::server::http_server_builder::IsSet,
203        S::Bind: crate::server::http_server_builder::IsUnset,
204        S::Ctx: crate::server::http_server_builder::IsUnset,
205    {
206        install_provider();
207
208        let addr = get_available_addr().expect("failed to get available address");
209        let (ctx, handler) = scuffle_context::Context::new();
210
211        let server = builder.bind(addr).ctx(ctx).build();
212
213        let handle = tokio::spawn(async move {
214            server.run().await.expect("server run failed");
215        });
216
217        // Wait for the server to start
218        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
219
220        let url = format!("https://{addr}/");
221
222        for version in versions {
223            let mut builder = reqwest::Client::builder().danger_accept_invalid_certs(true).https_only(true);
224
225            if *version == reqwest::Version::HTTP_3 {
226                builder = builder.http3_prior_knowledge();
227            } else if *version == reqwest::Version::HTTP_2 {
228                builder = builder.http2_prior_knowledge();
229            } else {
230                builder = builder.http1_only();
231            }
232
233            let client = builder.build().expect("failed to build client");
234
235            let request = client
236                .request(reqwest::Method::GET, &url)
237                .version(*version)
238                .body(RESPONSE_TEXT.to_string())
239                .build()
240                .expect("failed to build request");
241
242            let resp = client
243                .execute(request)
244                .await
245                .unwrap_or_else(|_| panic!("failed to get response version {version:?}"))
246                .text()
247                .await
248                .expect("failed to get text");
249
250            assert_eq!(resp, RESPONSE_TEXT);
251        }
252
253        handler.shutdown().await;
254        handle.await.expect("task failed");
255    }
256
257    #[tokio::test]
258    #[cfg(feature = "http2")]
259    async fn http2_server() {
260        let builder = HttpServer::builder().service_factory(service_clone_factory(fn_http_service(|_| async {
261            Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
262        })));
263
264        #[cfg(feature = "http1")]
265        let builder = builder.enable_http1(false);
266
267        test_server(builder, &[reqwest::Version::HTTP_2]).await;
268    }
269
270    #[tokio::test]
271    #[cfg(all(feature = "http1", feature = "http2"))]
272    async fn http12_server() {
273        let server = HttpServer::builder()
274            .service_factory(service_clone_factory(fn_http_service(|_| async {
275                Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
276            })))
277            .enable_http1(true)
278            .enable_http2(true);
279
280        test_server(server, &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2]).await;
281    }
282
283    #[cfg(feature = "tls-rustls")]
284    fn rustls_config() -> rustls::ServerConfig {
285        install_provider();
286
287        let certfile = std::fs::File::open(file_path("cert.pem")).expect("cert not found");
288        let certs = rustls_pemfile::certs(&mut std::io::BufReader::new(certfile))
289            .collect::<Result<Vec<_>, _>>()
290            .expect("failed to load certs");
291        let keyfile = std::fs::File::open(file_path("key.pem")).expect("key not found");
292        let key = rustls_pemfile::private_key(&mut std::io::BufReader::new(keyfile))
293            .expect("failed to load key")
294            .expect("no key found");
295
296        rustls::ServerConfig::builder()
297            .with_no_client_auth()
298            .with_single_cert(certs, key)
299            .expect("failed to build config")
300    }
301
302    #[tokio::test]
303    #[cfg(all(feature = "tls-rustls", feature = "http1"))]
304    async fn rustls_http1_server() {
305        let builder = HttpServer::builder()
306            .service_factory(service_clone_factory(fn_http_service(|_| async {
307                Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
308            })))
309            .rustls_config(rustls_config());
310
311        #[cfg(feature = "http2")]
312        let builder = builder.enable_http2(false);
313
314        test_tls_server(builder, &[reqwest::Version::HTTP_11]).await;
315    }
316
317    #[tokio::test]
318    #[cfg(all(feature = "tls-rustls", feature = "http3"))]
319    async fn rustls_http3_server() {
320        let builder = HttpServer::builder()
321            .service_factory(service_clone_factory(fn_http_service(|_| async {
322                Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
323            })))
324            .rustls_config(rustls_config())
325            .enable_http3(true);
326
327        #[cfg(feature = "http2")]
328        let builder = builder.enable_http2(false);
329
330        #[cfg(feature = "http1")]
331        let builder = builder.enable_http1(false);
332
333        test_tls_server(builder, &[reqwest::Version::HTTP_3]).await;
334    }
335
336    #[tokio::test]
337    #[cfg(all(feature = "tls-rustls", feature = "http1", feature = "http2"))]
338    async fn rustls_http12_server() {
339        let builder = HttpServer::builder()
340            .service_factory(service_clone_factory(fn_http_service(|_| async {
341                Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
342            })))
343            .rustls_config(rustls_config())
344            .enable_http1(true)
345            .enable_http2(true);
346
347        test_tls_server(builder, &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2]).await;
348    }
349
350    #[tokio::test]
351    #[cfg(all(feature = "tls-rustls", feature = "http1", feature = "http2", feature = "http3"))]
352    async fn rustls_http123_server() {
353        let builder = HttpServer::builder()
354            .service_factory(service_clone_factory(fn_http_service(|_| async {
355                Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
356            })))
357            .rustls_config(rustls_config())
358            .enable_http1(true)
359            .enable_http2(true)
360            .enable_http3(true);
361
362        test_tls_server(
363            builder,
364            &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2, reqwest::Version::HTTP_3],
365        )
366        .await;
367    }
368
369    #[tokio::test]
370    async fn no_backend() {
371        let addr = get_available_addr().expect("failed to get available address");
372
373        let builder = HttpServer::builder()
374            .service_factory(service_clone_factory(fn_http_service(|_| async {
375                Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
376            })))
377            .bind(addr);
378
379        #[cfg(feature = "http1")]
380        let builder = builder.enable_http1(false);
381
382        #[cfg(feature = "http2")]
383        let builder = builder.enable_http2(false);
384
385        builder
386            .build()
387            .run()
388            .with_timeout(Duration::from_millis(100))
389            .await
390            .expect("server timed out")
391            .expect("server failed");
392    }
393
394    #[tokio::test]
395    #[cfg(feature = "tls-rustls")]
396    async fn rustls_no_backend() {
397        let addr = get_available_addr().expect("failed to get available address");
398
399        let builder = HttpServer::builder()
400            .service_factory(service_clone_factory(fn_http_service(|_| async {
401                Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
402            })))
403            .rustls_config(rustls_config())
404            .bind(addr);
405
406        #[cfg(feature = "http1")]
407        let builder = builder.enable_http1(false);
408
409        #[cfg(feature = "http2")]
410        let builder = builder.enable_http2(false);
411
412        builder
413            .build()
414            .run()
415            .with_timeout(Duration::from_millis(100))
416            .await
417            .expect("server timed out")
418            .expect("server failed");
419    }
420
421    #[tokio::test]
422    #[cfg(all(feature = "tower", feature = "http1", feature = "http2"))]
423    async fn tower_make_service() {
424        let builder = HttpServer::builder()
425            .tower_make_service_factory(tower::service_fn(|_| async {
426                Ok::<_, Infallible>(tower::service_fn(|_| async move {
427                    Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
428                }))
429            }))
430            .enable_http1(true)
431            .enable_http2(true);
432
433        test_server(builder, &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2]).await;
434    }
435
436    #[tokio::test]
437    #[cfg(all(feature = "tower", feature = "http1", feature = "http2"))]
438    async fn tower_custom_make_service() {
439        let builder = HttpServer::builder()
440            .custom_tower_make_service_factory(
441                tower::service_fn(|target| async move {
442                    assert_eq!(target, 42);
443                    Ok::<_, Infallible>(tower::service_fn(|_| async move {
444                        Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
445                    }))
446                }),
447                42,
448            )
449            .enable_http1(true)
450            .enable_http2(true);
451
452        test_server(builder, &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2]).await;
453    }
454
455    #[tokio::test]
456    #[cfg(all(feature = "tower", feature = "http1", feature = "http2"))]
457    async fn tower_make_service_with_addr() {
458        use std::net::SocketAddr;
459
460        let builder = HttpServer::builder()
461            .tower_make_service_with_addr(tower::service_fn(|addr: SocketAddr| async move {
462                assert!(addr.ip().is_loopback());
463                Ok::<_, Infallible>(tower::service_fn(|_| async move {
464                    Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
465                }))
466            }))
467            .enable_http1(true)
468            .enable_http2(true);
469
470        test_server(builder, &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2]).await;
471    }
472
473    #[tokio::test]
474    #[cfg(all(feature = "http1", feature = "http2"))]
475    async fn fn_service_factory() {
476        use crate::service::fn_http_service_factory;
477
478        let builder = HttpServer::builder()
479            .service_factory(fn_http_service_factory(|_| async {
480                Ok::<_, Infallible>(fn_http_service(|_| async {
481                    Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
482                }))
483            }))
484            .enable_http1(true)
485            .enable_http2(true);
486
487        test_server(builder, &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2]).await;
488    }
489
490    #[tokio::test]
491    #[cfg(all(
492        feature = "http1",
493        feature = "http2",
494        feature = "http3",
495        feature = "tls-rustls",
496        feature = "tower"
497    ))]
498    async fn axum_service() {
499        let router = axum::Router::new().route(
500            "/",
501            axum::routing::get(|req: String| async move {
502                assert_eq!(req, RESPONSE_TEXT);
503                http::Response::new(RESPONSE_TEXT.to_string())
504            }),
505        );
506
507        let builder = HttpServer::builder()
508            .tower_make_service_factory(router.into_make_service())
509            .rustls_config(rustls_config())
510            .enable_http3(true)
511            .enable_http1(true)
512            .enable_http2(true);
513
514        test_tls_server(
515            builder,
516            &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2, reqwest::Version::HTTP_3],
517        )
518        .await;
519    }
520
521    #[tokio::test]
522    #[cfg(all(feature = "http1", feature = "http2"))]
523    async fn tracked_body() {
524        use crate::body::TrackedBody;
525
526        #[derive(Clone)]
527        struct TestTracker;
528
529        impl crate::body::Tracker for TestTracker {
530            type Error = Infallible;
531
532            fn on_data(&self, size: usize) -> Result<(), Self::Error> {
533                assert_eq!(size, RESPONSE_TEXT.len());
534                Ok(())
535            }
536        }
537
538        let builder = HttpServer::builder()
539            .service_factory(service_clone_factory(fn_http_service(|req| async {
540                let req = req.map(|b| TrackedBody::new(b, TestTracker));
541                let body = req.into_body();
542                Ok::<_, Infallible>(http::Response::new(body))
543            })))
544            .enable_http1(true)
545            .enable_http2(true);
546
547        test_server(builder, &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2]).await;
548    }
549
550    #[tokio::test]
551    #[cfg(all(feature = "http1", feature = "http2"))]
552    async fn tracked_body_error() {
553        use crate::body::TrackedBody;
554
555        #[derive(Clone)]
556        struct TestTracker;
557
558        impl crate::body::Tracker for TestTracker {
559            type Error = &'static str;
560
561            fn on_data(&self, size: usize) -> Result<(), Self::Error> {
562                assert_eq!(size, RESPONSE_TEXT.len());
563                Err("test")
564            }
565        }
566
567        let builder = HttpServer::builder()
568            .service_factory(service_clone_factory(fn_http_service(|req| async {
569                let req = req.map(|b| TrackedBody::new(b, TestTracker));
570                let body = req.into_body();
571                // Use axum to convert the body to bytes
572                let bytes = axum::body::to_bytes(axum::body::Body::new(body), usize::MAX).await;
573                assert_eq!(bytes.expect_err("expected error").to_string(), "tracker error: test");
574
575                Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
576            })))
577            .enable_http1(true)
578            .enable_http2(true);
579
580        test_server(builder, &[reqwest::Version::HTTP_11, reqwest::Version::HTTP_2]).await;
581    }
582
583    #[tokio::test]
584    #[cfg(all(feature = "http2", feature = "http3", feature = "tls-rustls"))]
585    async fn response_trailers() {
586        #[derive(Default)]
587        struct TestBody {
588            data_sent: bool,
589        }
590
591        impl http_body::Body for TestBody {
592            type Data = bytes::Bytes;
593            type Error = Infallible;
594
595            fn poll_frame(
596                mut self: std::pin::Pin<&mut Self>,
597                _cx: &mut std::task::Context<'_>,
598            ) -> std::task::Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
599                if !self.data_sent {
600                    self.as_mut().data_sent = true;
601                    let data = http_body::Frame::data(bytes::Bytes::from_static(RESPONSE_TEXT.as_bytes()));
602                    std::task::Poll::Ready(Some(Ok(data)))
603                } else {
604                    let mut trailers = http::HeaderMap::new();
605                    trailers.insert("test", "test".parse().unwrap());
606                    std::task::Poll::Ready(Some(Ok(http_body::Frame::trailers(trailers))))
607                }
608            }
609        }
610
611        let builder = HttpServer::builder()
612            .service_factory(service_clone_factory(fn_http_service(|_req| async {
613                let mut resp = http::Response::new(TestBody::default());
614                resp.headers_mut().insert("trailers", "test".parse().unwrap());
615                Ok::<_, Infallible>(resp)
616            })))
617            .rustls_config(rustls_config())
618            .enable_http3(true)
619            .enable_http2(true);
620
621        #[cfg(feature = "http1")]
622        let builder = builder.enable_http1(false);
623
624        test_tls_server(builder, &[reqwest::Version::HTTP_2, reqwest::Version::HTTP_3]).await;
625    }
626}