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