servlin/
lib.rs

1//! Servlin
2//! ========
3//! [![crates.io version](https://img.shields.io/crates/v/servlin.svg)](https://crates.io/crates/servlin)
4//! [![license: Apache 2.0](https://raw.githubusercontent.com/mleonhard/servlin/main/license-apache-2.0.svg)](http://www.apache.org/licenses/LICENSE-2.0)
5//! [![unsafe forbidden](https://raw.githubusercontent.com/mleonhard/servlin/main/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/)
6//! [![pipeline status](https://github.com/mleonhard/servlin/workflows/CI/badge.svg)](https://github.com/mleonhard/servlin/actions)
7//!
8//! A modular HTTP server library in Rust.
9//!
10//! # Features
11//! - `forbid(unsafe_code)`
12//! - Threaded request handlers:<br>
13//!   `FnOnce(Request) -> Response + 'static + Clone + Send + Sync`
14//! - Uses async code internally for excellent performance under load
15//! - JSON
16//! - Server-Sent Events (SSE)
17//! - Saves large request bodies to temp files
18//! - Sends 100-Continue
19//! - Limits number of threads and connections
20//! - Modular: roll your own logging, write custom versions of internal methods, etc.
21//! - No macros or complicated type params
22//! - Good test coverage (63%)
23//!
24//! # Limitations
25//! - New, not proven in production.
26//! - To do:
27//!   - Request timeouts
28//!   - `chunked` transfer-encoding for request bodies
29//!   - gzip
30//!   - brotli
31//!   - TLS
32//!   - automatically getting TLS certs via ACME
33//!   - Drop idle connections when approaching connection limit.
34//!   - Denial-of-Service mitigation: source throttling, minimum throughput
35//!   - Complete functional test suite
36//!   - Missing load tests
37//!   - Disk space usage limits
38//!
39//! # Examples
40//! Complete examples: [`examples/`](https://github.com/mleonhard/servlin/tree/main/examples).
41//!
42//! Simple example:
43//! ```rust
44//! use serde::Deserialize;
45//! use serde_json::json;
46//! use servlin::{
47//!     socket_addr_127_0_0_1,
48//!     Error,
49//!     HttpServerBuilder,
50//!     Request,
51//!     Response
52//! };
53//! use servlin::log::log_request_and_response;
54//! use std::sync::Arc;
55//! use temp_dir::TempDir;
56//!
57//! struct State {}
58//!
59//! fn hello(_state: Arc<State>, req: Request) -> Result<Response, Error> {
60//!     #[derive(Deserialize)]
61//!     struct Input {
62//!         name: String,
63//!     }
64//!     let input: Input = req.json()?;
65//!     Ok(Response::json(200, json!({"message": format!("Hello, {}!", input.name)}))?)
66//! }
67//!
68//! fn handle_req(state: Arc<State>, req: Request) -> Result<Response, Error> {
69//!     match (req.method(), req.url().path.as_str()) {
70//!         ("GET", "/ping") => Ok(Response::text(200, "ok")),
71//!         ("POST", "/hello") => hello(state, req),
72//!         _ => Ok(Response::text(404, "Not found")),
73//!     }
74//! }
75//!
76//! let state = Arc::new(State {});
77//! let request_handler = move |req: Request| {
78//!     log_request_and_response(req, |req| handle_req(state, req)).unwrap()
79//! };
80//! let cache_dir = TempDir::new().unwrap();
81//! safina::timer::start_timer_thread();
82//! let executor = safina::executor::Executor::new(1, 9).unwrap();
83//! # let permit = permit::Permit::new();
84//! # let server_permit = permit.new_sub();
85//! # std::thread::spawn(move || {
86//! #     std::thread::sleep(std::time::Duration::from_millis(100));
87//! #     drop(permit);
88//! # });
89//! executor.block_on(
90//!     HttpServerBuilder::new()
91//! #       .permit(server_permit)
92//!         .listen_addr(socket_addr_127_0_0_1(8271))
93//!         .max_conns(1000)
94//!         .small_body_len(64 * 1024)
95//!         .receive_large_bodies(cache_dir.path())
96//!         .spawn_and_join(request_handler)
97//! ).unwrap();
98//! ```
99//! # Cargo Geiger Safety Report
100//! # Alternatives
101//! See [rust-webserver-comparison.md](https://github.com/mleonhard/servlin/blob/main/rust-webserver-comparison.md).
102//!
103//! # Changelog
104//! - v0.8.0 2025-07-06 - Use own [Url] struct.
105//! - v0.7.0 2025-06-30
106//!    - Require Rust 2024 edition.
107//!   - `log_request_and_response` to log `duration_ms` tag.
108//!   - Fix typo in function name `Response::internal_server_errror_500`.
109//!   - Close connection on 5xx error.
110//!   - Acceptor thread to log errors, not panic.
111//!   - Add [`Request::parse_url`].
112//!   - Add [`Response::too_many_requests_429`].
113//!   - Implement `Into<TagList>` for arrays.
114//!   - Support asterisk request target.
115//!   - Use `safina` v0.7.
116//! - v0.6.0 2024-11-02
117//!   - Remove `servlin::reexports` module.
118//!   - Use `safina` v0.6.0.
119//! - v0.5.1 2024-10-26 - Remove dependency on `once_cell`.
120//! - v0.5.0 2024-10-21 - Remove `LogFileWriterBuilder`.
121//! - v0.4.3 - Implement `From<Cow<'_, str>>` and `From<&Path>` for `TagValue`.
122//! - v0.4.2 - Implement `Seek` for `BodyReader`.
123//! - v0.4.1
124//!   - Add `Request::opt_json`.
125//!   - Implement `From<LoggerStoppedError>` for `Error`.
126//! - v0.4.0
127//!   - Changed `Response::json` to return `Result<Response, Error>`.
128//!   - Changed `log_request_and_response` to return `Result`.
129//!   - Added `Response::unprocessable_entity_422`.
130//! - v0.3.2 - Fix bug in `Response::include_dir` redirects.
131//! - v0.3.1
132//!   - Add `Response::redirect_301`
133//!   - `Response::include_dir` to redirect from `/somedir` to `/somedir/` so relative URLs will work.
134//! - v0.3.0 - Changed `Response::include_dir` to take `&Request` and look for `index.html` in dirs.
135//! - v0.2.0
136//!   - Added:
137//!     - `log_request_and_response` and other logging tooling
138//!     - `Response::ok_200()`
139//!     - `Response::unauthorized_401()`
140//!     - `Response::forbidden_403()`
141//!     - `Response::internal_server_errror_500()`
142//!     - `Response::not_implemented_501()`
143//!     - `Response::service_unavailable_503()`
144//!     - `EventSender::is_connected()`
145//!     - `PORT_env()`
146//!   - Removed `print_log_response` and `RequestBody::length_is_known`
147//!   - Changed `RequestBody::len` and `is_empty` to return `Option`.
148//!   - Bugfixes
149//! - v0.1.1 - Add `EventSender::unconnected`.
150//! - v0.1.0 - Rename library to Servlin.
151//!
152//! # TO DO
153//! - Fix limitations above
154//! - Support [HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD)
155//!   responses that have Content-Length set and no body.
156//! - Add a server-wide limit on upload body size.
157//! - Limit disk usage for caching uploads.
158//! - Update `rust-webserver-comparison.md`
159//!   - Add missing data
160//!   - Add other servers from <https://www.arewewebyet.org/topics/frameworks/>
161//!   - Rearrange
162//!   - Generate geiger reports for each web server
163#![forbid(unsafe_code)]
164mod accept;
165mod ascii_string;
166mod body_async_reader;
167mod body_reader;
168mod content_type;
169mod cookie;
170mod error;
171mod event;
172mod head;
173mod headers;
174mod http_conn;
175mod http_error;
176pub mod log;
177mod rand;
178mod request;
179mod request_body;
180mod response;
181mod response_body;
182mod time;
183mod token_set;
184mod url;
185mod util;
186
187pub use crate::accept::{
188    PORT_env, socket_addr_127_0_0_1, socket_addr_127_0_0_1_any_port, socket_addr_all_interfaces,
189};
190pub use crate::ascii_string::AsciiString;
191pub use crate::body_async_reader::BodyAsyncReader;
192pub use crate::body_reader::BodyReader;
193pub use crate::content_type::ContentType;
194pub use crate::cookie::{Cookie, SameSite};
195pub use crate::error::Error;
196pub use crate::event::{Event, EventSender};
197pub use crate::headers::{Header, HeaderList};
198pub use crate::http_conn::HttpConn;
199pub use crate::request::Request;
200pub use crate::request_body::RequestBody;
201pub use crate::response::Response;
202pub use crate::response_body::ResponseBody;
203pub use crate::url::{PercentEncodePurpose, Url, UrlParseError, percent_decode, percent_encode};
204
205/// This part of the library is not covered by the semver guarantees.
206/// If you use these in your program, a minor version upgrade could break your build.
207///
208/// If you use these items in a published library,
209/// your library should depend on a specific version of this library.
210pub mod internal {
211    pub use crate::accept::*;
212    pub use crate::body_async_reader::*;
213    pub use crate::body_reader::*;
214    pub use crate::content_type::*;
215    pub use crate::cookie::*;
216    pub use crate::event::*;
217    pub use crate::head::*;
218    pub use crate::headers::*;
219    pub use crate::http_conn::*;
220    pub use crate::http_error::*;
221    pub use crate::request::*;
222    pub use crate::request_body::*;
223    pub use crate::response::*;
224    pub use crate::response_body::*;
225    pub use crate::time::*;
226    pub use crate::token_set::*;
227    pub use crate::util::*;
228}
229
230use crate::accept::accept_loop;
231use crate::http_conn::handle_http_conn;
232use crate::token_set::TokenSet;
233use async_net::TcpListener;
234use permit::Permit;
235use std::net::SocketAddr;
236use std::path::PathBuf;
237
238/// Builds an HTTP server.
239pub struct HttpServerBuilder {
240    opt_cache_dir: Option<PathBuf>,
241    listen_addr: SocketAddr,
242    max_conns: usize,
243    small_body_len: usize,
244    permit: Permit,
245}
246impl HttpServerBuilder {
247    /// Makes a new builder these default settings:
248    /// - Listens on 127.0.0.1
249    /// - Picks a random port
250    /// - 100 max connections
251    /// - 64 KiB small body length
252    /// - no cache dir, server rejects large request bodies
253    #[allow(clippy::new_without_default)]
254    #[must_use]
255    pub fn new() -> Self {
256        Self {
257            opt_cache_dir: None,
258            listen_addr: socket_addr_127_0_0_1_any_port(),
259            max_conns: 100,
260            small_body_len: 64 * 1024,
261            permit: Permit::new(),
262        }
263    }
264
265    #[must_use]
266    pub fn listen_addr(mut self, addr: SocketAddr) -> Self {
267        self.listen_addr = addr;
268        self
269    }
270
271    /// Sets the maximum number of connections to handle at one time.
272    ///
273    /// When the server is handling the maximum number of connections,
274    /// it waits for a connection to drop before accepting new ones.
275    ///
276    /// Each connection uses a file handle.
277    /// Some processes run with a limit on the number of file handles.
278    /// The OS kernel also has a limit for all processes combined.
279    ///
280    /// # Panics
281    /// Panics when `n` is zero.
282    #[must_use]
283    pub fn max_conns(mut self, n: usize) -> Self {
284        assert!(n > 0, "refusing to set max_conns to zero");
285        self.max_conns = n;
286        self
287    }
288
289    /// Save large request bodies to this directory.
290    ///
291    /// If you do not call this method, the server will refuse all
292    /// requests with bodies larger than `small_body_len` with `413 Payload Too Large`.
293    /// It will also refuse all bodies with unknown length.
294    ///
295    /// # Example
296    /// ```
297    /// use servlin::{HttpServerBuilder, Request, Response};
298    /// use std::io::Read;
299    ///
300    /// let cache_dir = temp_dir::TempDir::new().unwrap();
301    /// let handler = move |req: Request| {
302    ///     if req.body.is_pending() {
303    ///         return Response::get_body_and_reprocess(1024 * 1024);
304    ///     }
305    ///     let len = req.body.reader().unwrap().bytes().count();
306    ///     Response::text(200, format!("body len={}", len))
307    /// };
308    /// # let permit = permit::Permit::new();
309    /// # let server_permit = permit.new_sub();
310    /// # std::thread::spawn(move || {
311    /// #     std::thread::sleep(std::time::Duration::from_millis(100));
312    /// #     drop(permit);
313    /// # });
314    /// safina::timer::start_timer_thread();
315    /// safina::executor::Executor::new(1, 1)
316    ///   .unwrap()
317    ///   .block_on(
318    ///     HttpServerBuilder::new()
319    /// #     .permit(server_permit)
320    ///       .receive_large_bodies(cache_dir.path())
321    ///       .spawn_and_join(handler)
322    ///   )
323    ///   .unwrap();
324    /// ```
325    #[must_use]
326    pub fn receive_large_bodies(mut self, cache_dir: &std::path::Path) -> Self {
327        self.opt_cache_dir = Some(cache_dir.to_path_buf());
328        self
329    }
330
331    /// Automatically receive request bodies up to length `n`,
332    /// saving them in memory.
333    ///
334    /// The default value is 64 KiB.
335    ///
336    /// Reject larger requests with `413 Payload Too Large`.
337    /// See [`receive_large_bodies`](ServerBuilder::receive_large_bodies).
338    ///
339    /// You can estimate the server memory usage with:
340    /// `small_body_len * max_conns`.
341    /// Using the default settings: 64 KiB * 100 connections => 6.4 MiB.
342    #[must_use]
343    pub fn small_body_len(mut self, n: usize) -> Self {
344        self.small_body_len = n;
345        self
346    }
347
348    /// Sets the permit used by the server.
349    ///
350    /// Revoke the permit to make the server gracefully shut down.
351    ///
352    /// # Example
353    /// ```
354    /// use std::net::SocketAddr;
355    /// use std::sync::Arc;
356    /// use permit::Permit;    
357    /// use safina::executor::Executor;
358    /// use servlin::{Response, HttpServerBuilder};
359    /// # fn do_some_requests(addr: SocketAddr) -> Result<(),()> { Ok(()) }
360    ///
361    /// safina::timer::start_timer_thread();
362    /// let executor: Arc<Executor> = Arc::default();
363    /// let permit = Permit::new();
364    /// let (addr, stopped_receiver) = executor.block_on(
365    ///     HttpServerBuilder::new()
366    ///         .permit(permit.new_sub())
367    ///         .spawn(move |req| Response::text(200, "yo"))
368    /// ).unwrap();
369    /// do_some_requests(addr).unwrap();
370    /// drop(permit); // Tell server to shut down.
371    /// stopped_receiver.recv().unwrap(); // Wait for server to stop.
372    /// ```
373    #[must_use]
374    pub fn permit(mut self, p: Permit) -> Self {
375        self.permit = p;
376        self
377    }
378
379    /// Spawns the server task.
380    ///
381    /// Returns `(addr, stopped_receiver)`.
382    /// The server is listening on `addr`.
383    /// After the server gracefully shuts down, it sends a message on `stopped_receiver`.
384    ///
385    /// # Errors
386    /// Returns an error when it fails to bind to the [`listen_addr`](ServerBuilder::listen_addr).
387    pub async fn spawn<F>(
388        self,
389        request_handler: F,
390    ) -> Result<(SocketAddr, safina::sync::Receiver<()>), std::io::Error>
391    where
392        F: FnOnce(Request) -> Response + 'static + Clone + Send + Sync,
393    {
394        let async_request_handler = |req: Request| async move {
395            let request_handler_clone = request_handler.clone();
396            // TODO: Handle threadpool backpressure.
397            //   - Keep set of pending requests, worker threads pull from it.
398            //   - Async task checks and removes disconnected connections
399            //   - send 503s
400            //   - priorities
401            //   - rate limits
402            //   - timeout request handlers with permit
403            safina::executor::schedule_blocking(move || request_handler_clone(req))
404                .await
405                .unwrap_or_else(|_| Response::text(500, "Server error"))
406        };
407        let conn_handler = move |permit, token, stream: async_net::TcpStream, addr| {
408            let http_conn = HttpConn::new(addr, stream);
409            safina::executor::spawn(handle_http_conn(
410                permit,
411                token,
412                http_conn,
413                self.opt_cache_dir,
414                self.small_body_len,
415                async_request_handler,
416            ));
417        };
418        let listener = TcpListener::bind(self.listen_addr).await?;
419        let addr = listener.local_addr()?;
420        let token_set = TokenSet::new(self.max_conns);
421        let (sender, receiver) = safina::sync::oneshot();
422        safina::executor::spawn(async move {
423            // Let's not make spawn accept_loop tasks, since that reduces throughput.
424            // To speed this up, we could use a separate accepter thread, or multiple threads.
425            accept_loop(self.permit, listener, token_set, conn_handler).await;
426            // TODO: Wait for connection tokens to return.
427            let _ignored = sender.send(());
428        });
429        Ok((addr, receiver))
430    }
431
432    /// Spawns the server task and waits for it to shutdown gracefully.
433    ///
434    /// # Errors
435    /// Returns an error when it fails to bind to the [`listen_addr`](ServerBuilder::listen_addr).
436    pub async fn spawn_and_join<F>(self, request_handler: F) -> Result<(), std::io::Error>
437    where
438        F: FnOnce(Request) -> Response + 'static + Clone + Send + Sync,
439    {
440        let (_addr, mut stopped_receiver) = self.spawn(request_handler).await?;
441        let _ignored = stopped_receiver.async_recv().await;
442        Ok(())
443    }
444}