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