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