Skip to main content

rust_web_server/
lib.rs

1//! # rust-web-server
2//!
3//! An HTTP/1.1, HTTP/2, and HTTP/3 (QUIC) server, reverse proxy, and
4//! application framework in one crate. HTTP/1.1 parsing, routing, middleware,
5//! JSON, WebSocket, and SSE are implemented in-crate; HTTP/2 and HTTP/3
6//! framing use the `h2` and `quinn`/`h3` crates, and TLS uses `rustls`.
7//!
8//! ## Use as a library
9//!
10//! Add to `Cargo.toml`:
11//!
12//! ```toml
13//! [dependencies]
14//! rust-web-server = "17"
15//! ```
16//!
17//! ## Quick start
18//!
19//! ```rust,no_run
20//! use rust_web_server::prelude::*;
21//!
22//! fn hello(_: &Request, _: &PathParams, _: &ConnectionInfo, _: &()) -> Response {
23//!     Response::text(STATUS_CODE_REASON_PHRASE.n200_ok, "Hello, world!")
24//! }
25//!
26//! fn main() {
27//!     let app = App::with_state(()).get("/hello", hello);
28//!     let (listener, pool) = Server::setup().unwrap();
29//!     Server::run(listener, pool, app);
30//! }
31//! ```
32//!
33//! See [DEVELOPER.md](https://github.com/bohdaq/rust-web-server/blob/main/DEVELOPER.md)
34//! for the full building blocks reference and use case examples.
35
36// Allows `::rust_web_server::…` paths to resolve from within this crate's own
37// tests, which is required by proc-macro derive output that uses that prefix.
38extern crate self as rust_web_server;
39
40pub mod app;
41#[cfg(feature = "auth")]
42pub mod auth;
43
44#[cfg(feature = "macros")]
45pub use rws_macros::{delete, get, patch, post, put, route, Config, FromRequest, JsonSchema, Validate};
46#[cfg(all(feature = "macros", any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql")))]
47pub use rws_macros::Model;
48#[cfg(feature = "http2")]
49pub mod async_state;
50#[cfg(feature = "http2")]
51pub(crate) mod async_bridge;
52pub mod session;
53// Socket-coupled — see spec/WASM_SHIM.md for what a wasm32-wasip2 build
54// excludes and why (no OS threads, no raw TCP/TLS in a WASI guest).
55#[cfg(not(target_arch = "wasm32"))]
56pub(crate) mod redis_protocol;
57pub mod sse;
58pub mod compression;
59pub mod cookie;
60pub mod error;
61pub mod extract;
62pub mod i18n;
63pub mod a2a;
64#[cfg(all(unix, any(feature = "http1", feature = "http2")))]
65pub mod systemd;
66pub mod ip_filter;
67pub mod macros;
68pub mod blocklist;
69pub mod cache;
70pub mod early_hints;
71pub mod disk_guard;
72pub mod traffic;
73pub mod authz;
74pub mod config_reload;
75#[cfg(not(target_arch = "wasm32"))]
76pub mod config_watch;
77pub mod config_check;
78/// RSA public keys and RS256 / rsa-sha256 signature verification (no `rsa` crate).
79#[cfg(any(feature = "auth-asymmetric", feature = "sso"))]
80pub mod rsa_key;
81pub mod server_config;
82pub mod feature;
83pub mod maintenance;
84pub mod metrics;
85pub mod mcp;
86pub mod request_log;
87pub mod request_id;
88#[cfg(not(target_arch = "wasm32"))]
89pub mod otel;
90#[cfg(feature = "acme")]
91pub mod acme;
92pub mod middleware;
93pub mod rate_limit;
94pub mod router;
95pub mod schema;
96pub mod state;
97pub mod test_client;
98pub mod application;
99pub mod body;
100pub mod client_hint;
101pub mod controller;
102pub mod core;
103pub mod cors;
104pub mod entry_point;
105pub mod ext;
106pub mod header;
107pub mod http;
108pub mod json;
109pub mod language;
110#[cfg(not(target_arch = "wasm32"))]
111pub mod log;
112pub mod mime_type;
113pub mod null;
114pub mod range;
115pub mod request;
116pub mod response;
117pub mod server;
118pub mod symbol;
119#[cfg(not(target_arch = "wasm32"))]
120pub mod thread_pool;
121pub mod url;
122pub mod pagination;
123#[cfg(not(target_arch = "wasm32"))]
124pub mod proxy;
125pub mod rewrite;
126pub mod scheduler;
127#[cfg(not(target_arch = "wasm32"))]
128pub mod tcp_proxy;
129#[cfg(not(target_arch = "wasm32"))]
130pub mod udp_proxy;
131#[cfg(not(target_arch = "wasm32"))]
132pub mod ws_proxy;
133#[cfg(not(target_arch = "wasm32"))]
134pub mod canary;
135#[cfg(not(target_arch = "wasm32"))]
136pub mod circuit_breaker;
137// `BackendPool`/`DiscoverySource` (DNS/etcd/Consul/Docker) are native-only —
138// gated internally in service_discovery/mod.rs, not at this declaration —
139// because `json_lite` (a plain JSON parser, no sockets) must stay available
140// for `secrets`'s Vault/AWS-SM/Key-Vault backends on every target.
141pub mod service_discovery;
142/// DNS wire format, shared by the SRV service discovery and ACME's DNS-01.
143pub(crate) mod dns;
144pub mod config_binding;
145pub mod di;
146#[cfg(not(target_arch = "wasm32"))]
147pub mod proxy_config;
148#[cfg(not(target_arch = "wasm32"))]
149pub mod llm;
150#[cfg(not(target_arch = "wasm32"))]
151pub mod admin;
152/// FastCGI client: PHP through php-fpm (18.21.0).
153#[cfg(not(target_arch = "wasm32"))]
154pub mod fastcgi;
155/// `rws ctl` / `rwsctl`: the admin API from the command line (18.20.0).
156#[cfg(not(target_arch = "wasm32"))]
157pub mod ctl;
158#[cfg(feature = "wasm-plugins")]
159pub mod wasm_plugin;
160#[cfg(not(target_arch = "wasm32"))]
161pub mod ingress;
162#[cfg(feature = "tera")]
163pub mod template;
164pub mod validate;
165pub mod virtual_host;
166#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
167pub mod model;
168#[cfg(not(target_arch = "wasm32"))]
169pub mod websocket;
170// Has both a native (TcpStream/rustls) and a wasm32 (wasi:http outgoing-handler)
171// backend behind Client::send() — see the #[cfg(target_arch = "wasm32")]
172// `send_once` in this module. spec/WASM_SHIM.md Phase 2 item 7.
173pub mod http_client;
174#[cfg(feature = "crypto")]
175pub mod crypto;
176#[cfg(feature = "csrf")]
177pub mod csrf;
178#[cfg(feature = "sso")]
179pub mod sso;
180// SMTP is a raw-TCP protocol with no wasi:http equivalent (unlike
181// http_client, which the host can proxy via outgoing-handler) — genuinely
182// can't work in a wasi:http guest, not just ungated. spec/WASM_SHIM.md.
183#[cfg(all(feature = "mailer", not(target_arch = "wasm32")))]
184pub mod mailer;
185#[cfg(feature = "jobs")]
186pub mod jobs;
187// Also compiled under `secrets` — `secrets::aws_secrets_manager`/`azure_key_vault`
188// reuse `StorageError` and the SigV4/Managed-Identity credential helpers from here.
189#[cfg(any(feature = "storage-local", feature = "storage-s3", feature = "storage-azure", feature = "storage-gcs", feature = "secrets"))]
190pub mod storage;
191#[cfg(feature = "openapi")]
192pub mod openapi;
193#[cfg(feature = "tower")]
194pub mod tower;
195#[cfg(feature = "gateway-api")]
196pub mod gateway_api;
197#[cfg(feature = "webhook")]
198pub mod webhook;
199#[cfg(feature = "secrets")]
200pub mod secrets;
201// Runs wrapped work on a background OS thread to bound wait time — needs
202// `std::thread::spawn`, unavailable on wasm32-wasip2. See spec/WASM_SHIM.md.
203#[cfg(not(target_arch = "wasm32"))]
204pub mod timeout;
205pub mod prelude;
206
207#[cfg(feature = "http2")]
208#[doc(hidden)]
209pub mod tls;
210
211#[cfg(feature = "http2")]
212#[doc(hidden)]
213pub mod h2_server;
214
215#[cfg(feature = "http3")]
216#[doc(hidden)]
217pub mod h3_handler;
218
219/// Shared infrastructure for tests that write process-wide environment variables.
220///
221/// Tests that call `override_environment_variables_from_config` or
222/// `CommandLineArgument::set_environment_variable` must hold this lock for
223/// their entire duration so they don't race with other tests reading the
224/// same variables.
225#[cfg(test)]
226mod stack_depth_tests;
227
228/// A rustls client-config builder with this crate's crypto provider
229/// (aws-lc-rs) named explicitly. `rustls::ClientConfig::builder()` uses the
230/// process-wide default provider and panics when none is installed and more
231/// than one is compiled in — which happens as soon as any dependency enables
232/// rustls's `ring` backend (e.g. sqlx). Every outbound TLS client in the
233/// crate starts here.
234#[cfg(all(any(feature = "http-client", feature = "http2"), not(target_arch = "wasm32")))]
235pub(crate) fn tls_client_builder() -> rustls::ConfigBuilder<rustls::ClientConfig, rustls::WantsVerifier> {
236    rustls::ClientConfig::builder_with_provider(std::sync::Arc::new(rustls::crypto::aws_lc_rs::default_provider()))
237        .with_safe_default_protocol_versions()
238        .expect("aws-lc-rs supports rustls's default protocol versions")
239}
240
241#[cfg(test)]
242pub mod test_env {
243    use std::sync::{Mutex, OnceLock};
244    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
245    pub fn lock() -> std::sync::MutexGuard<'static, ()> {
246        LOCK.get_or_init(|| Mutex::new(()))
247            .lock()
248            .unwrap_or_else(|e| e.into_inner())
249    }
250
251    /// A per-process path under the OS temp dir that is deleted (file or
252    /// directory) when dropped — so a test cleans up even when an assertion
253    /// fails before its last line.
254    pub struct TempPath(std::path::PathBuf);
255
256    impl TempPath {
257        pub fn new(name: &str) -> Self {
258            TempPath(std::env::temp_dir().join(format!("rws_test_{}_{}", std::process::id(), name)))
259        }
260
261        /// The same in the working directory (the static-file root), for
262        /// tests that need a file the server would serve.
263        pub fn new_in_cwd(name: &str) -> Self {
264            TempPath(std::env::current_dir().expect("working directory").join(format!("rws_test_{}_{}", std::process::id(), name)))
265        }
266
267        pub fn as_str(&self) -> &str {
268            self.0.to_str().expect("temp dir path is UTF-8")
269        }
270    }
271
272    impl std::ops::Deref for TempPath {
273        type Target = std::path::Path;
274        fn deref(&self) -> &std::path::Path {
275            &self.0
276        }
277    }
278
279    impl Drop for TempPath {
280        fn drop(&mut self) {
281            // A symlink is removed itself, never followed (a test may link
282            // to a system directory).
283            let is_link = std::fs::symlink_metadata(&self.0).is_ok_and(|m| m.file_type().is_symlink());
284            if is_link {
285                let _ = std::fs::remove_file(&self.0);
286            } else if self.0.is_dir() {
287                let _ = std::fs::remove_dir_all(&self.0);
288            } else {
289                let _ = std::fs::remove_file(&self.0);
290            }
291        }
292    }
293}
294
295/// Entry points for `fuzz/` targets that reach crate-private parsers.
296/// Compiled only under `--cfg fuzzing`, which `cargo fuzz` sets — never part
297/// of the public API.
298#[cfg(fuzzing)]
299#[doc(hidden)]
300pub mod fuzz_api {
301    /// Proxy route-matching normalization of a request target (#50): any
302    /// input, and normalizing twice changes nothing.
303    pub fn proxy_normalize_target(data: &[u8]) {
304        let Ok(target) = std::str::from_utf8(data) else { return };
305        let once = crate::proxy_config::normalize_request_target(target).into_owned();
306        let twice = crate::proxy_config::normalize_request_target(&once).into_owned();
307        assert_eq!(once, twice, "normalization is not idempotent for {:?}", target);
308    }
309
310    /// Static-file request-path decoding (#55). Must never panic; the
311    /// decoded path never carries a `..` segment past the web-root check.
312    pub fn static_path_decode(data: &[u8]) {
313        let Ok(path) = std::str::from_utf8(data) else { return };
314        if let Ok(fs_path) = crate::app::controller::static_resource::static_fs_path(path) {
315            let relative = &fs_path[crate::app::controller::static_resource::working_directory().len()..];
316            assert!(!relative.split(['/', '\\']).any(|segment| segment == ".."), "{:?} -> {:?}", path, fs_path);
317        }
318    }
319
320    /// OpenAPI documents to MCP tools (#66): any bytes. Must never panic;
321    /// every tool's schemas are JSON and its requests build or err cleanly.
322    pub fn openapi_to_mcp(data: &[u8]) {
323        let Ok(spec) = std::str::from_utf8(data) else { return };
324        let options = crate::mcp::openapi_tools::ToolOptions::default();
325        let Ok(found) = crate::mcp::openapi_tools::tools(spec, &options) else { return };
326        for tool in found {
327            assert!(crate::service_discovery::json_lite::parse(&tool.input_schema).is_ok());
328            assert!(tool.name.len() <= 64 && tool.name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-'), "{}", tool.name);
329            let _ = tool.request(r#"{"id":"1","body":{"a":[1,2]},"q":["x","y"]}"#);
330        }
331    }
332
333    /// An A2A JSON-RPC request body (#65): any bytes. Must never panic, and
334    /// the answer is always a JSON-RPC response (or a 413).
335    pub fn a2a_request(data: &[u8]) {
336        use crate::application::Application;
337        thread_local! {
338            static AGENT: crate::a2a::A2aServer = crate::a2a::A2aServer::new(crate::a2a::AgentCard::new("fuzz", "fuzz", "1"))
339                .on_message(|ctx, message| match message.text().len() % 3 {
340                    0 => crate::a2a::A2aReply::Message(crate::a2a::Message::agent_text(message.text())),
341                    1 => crate::a2a::A2aReply::Task(ctx.input_required("more?")),
342                    _ => crate::a2a::A2aReply::Task(ctx.complete(vec![crate::a2a::Artifact::text("a", message.text())])),
343                });
344        }
345        let request = crate::request::Request {
346            method: "POST".to_string(),
347            request_uri: "/a2a".to_string(),
348            http_version: "HTTP/1.1".to_string(),
349            headers: vec![crate::header::Header::new("A2A-Version", "1.0".to_string())],
350            body: data.to_vec().into(),
351        };
352        let connection = crate::server::ConnectionInfo::default();
353        let response = AGENT.with(|agent| agent.execute(&request, &connection)).expect("A2A never errs");
354        if response.status_code == 200 {
355            let body = String::from_utf8(response.content_range_list[0].body.clone()).expect("UTF-8");
356            assert!(crate::service_discovery::json_lite::parse(&body).is_ok(), "not JSON: {body}");
357        }
358    }
359
360    /// `Accept-Language` parsing and negotiation (#33): any header value;
361    /// at most `MAX_LANGUAGE_RANGES` ranges, and the language chosen is
362    /// always one of those offered.
363    pub fn accept_language(data: &[u8]) {
364        let Ok(value) = std::str::from_utf8(data) else { return };
365        let preferences = crate::i18n::AcceptLanguage::parse(value);
366        assert!(preferences.0.len() <= crate::i18n::MAX_LANGUAGE_RANGES);
367        let available = ["en", "en-GB", "uk", "pt-BR"];
368        if let Some(chosen) = preferences.negotiate(&available) {
369            assert!(available.contains(&chosen));
370        }
371    }
372
373    /// i18n catalog text (#33): any input; whatever parses translates
374    /// without panicking, plurals and placeholders included.
375    pub fn i18n_catalog(data: &[u8]) {
376        let Ok(text) = std::str::from_utf8(data) else { return };
377        if let Ok(catalogs) = crate::i18n::Catalogs::new("en").add("en", text) {
378            let t = catalogs.for_language("en");
379            for line in text.lines() {
380                let key = line.split('=').next().unwrap_or("").trim();
381                let _ = t.t_with(key, &[("name", "x")]);
382                let _ = t.plural(key.trim_end_matches(".one").trim_end_matches(".other"), 3);
383            }
384        }
385    }
386
387    /// `Accept-Encoding` negotiation (#7): any header value; the choice is
388    /// always one of the offered codings, and a coding the value refuses
389    /// (its first `q=` parameter is 0) is never chosen.
390    pub fn accept_encoding(data: &[u8]) {
391        let Ok(value) = std::str::from_utf8(data) else { return };
392        let request = crate::request::Request {
393            method: "GET".to_string(),
394            request_uri: "/".to_string(),
395            http_version: "HTTP/1.1".to_string(),
396            headers: vec![crate::header::Header::new(crate::header::Header::_ACCEPT_ENCODING, value.to_string())],
397            body: Default::default(),
398        };
399        for preference in [crate::compression::DYNAMIC_PREFERENCE, crate::compression::STATIC_PREFERENCE] {
400            if let Some(chosen) = crate::compression::negotiate(&request, preference) {
401                assert!(preference.contains(&chosen));
402                // Refused: the coding's first `q=` parameter is exactly 0.
403                let refused = value.split(',').any(|c| {
404                    let mut parts = c.split(';');
405                    parts.next().unwrap_or("").trim().eq_ignore_ascii_case(chosen.token())
406                        && parts
407                            .map(str::trim)
408                            .find(|p| p.as_bytes().get(..2).is_some_and(|b| b.eq_ignore_ascii_case(b"q=")))
409                            .is_some_and(|q| q[2..].trim().parse::<f32>() == Ok(0.0))
410                });
411                let listed_once = value.split(',').filter(|c| c.split(';').next().unwrap_or("").trim().eq_ignore_ascii_case(chosen.token())).count() == 1;
412                assert!(!(refused && listed_once), "{value:?} refused {chosen:?}");
413            }
414        }
415    }
416
417    /// What a WebAssembly plugin returns, as the host reads it (#40).
418    /// Must never panic.
419    #[cfg(feature = "wasm-plugins")]
420    pub fn wasm_plugin_output(data: &[u8]) {
421        let _ = crate::wasm_plugin::parse_request_output(data);
422        let _ = crate::wasm_plugin::parse_response_output(data);
423    }
424
425    /// A disk-cache entry file as the cache reads it back (#39): the header
426    /// line and key, then the stored response. Must never panic.
427    pub fn cache_entry(data: &[u8]) {
428        if let Some((_, _, _, offset)) = crate::cache::disk::parse_entry_header(data) {
429            let _ = crate::response::Response::parse_upstream(&data[offset..], crate::proxy::REGENERATED_OR_HOP_BY_HOP);
430        }
431    }
432
433    /// A request as the traffic classifier sees it: the first line is the
434    /// User-Agent, the rest the path; the address comes from the first byte.
435    pub fn traffic_classify(data: &[u8]) {
436        let text = String::from_utf8_lossy(data);
437        let (ua, path) = text.split_once('\n').unwrap_or((&text, "/"));
438        let ip = std::net::IpAddr::from([data.first().copied().unwrap_or(1), 2, 3, 4]);
439        let class = crate::traffic::classify(ip, Some(ua), path);
440        let _ = (class.group(), class.kind.label());
441    }
442
443    /// Authorization text as the admin API and config file read it (#32):
444    /// a tuple, a subject, and a relation definition.
445    pub fn authz_parse(data: &[u8]) {
446        let text = String::from_utf8_lossy(data);
447        let _ = crate::authz::Tuple::parse(&text);
448        let _ = crate::authz::Subject::parse(&text);
449        if let Ok(terms) = crate::authz::rebac::parse_expression(&text) {
450            let _ = terms.len();
451            let _ = crate::authz::Schema::new().relation("t", "r", &text).and_then(|s| s.validate());
452        }
453    }
454
455    /// An `Available-Dictionary` header value, and dictionary-compressed
456    /// bodies through both decoders (#38).
457    pub fn available_dictionary(data: &[u8]) {
458        use crate::compression::dictionary::{decode, parse_available_dictionary, DictionaryEncoding};
459        let _ = parse_available_dictionary(&String::from_utf8_lossy(data));
460        for encoding in [DictionaryEncoding::Dcz, DictionaryEncoding::Dcb] {
461            let _ = decode(encoding, data, b"dictionary", 1 << 16);
462        }
463    }
464
465    /// Provider responses as the LLM proxy reads them (#41): usage from a
466    /// JSON body or an event stream fed in pieces, and chunked framing.
467    /// Must never panic.
468    pub fn llm_usage(data: &[u8]) {
469        for content_type in ["application/json", "text/event-stream"] {
470            let mut parser = crate::llm::UsageParser::new(content_type);
471            for piece in data.chunks(7) {
472                parser.feed(piece);
473            }
474            let _ = parser.finish();
475        }
476        let mut decoder = crate::llm::usage::ChunkDecoder::default();
477        let mut out = Vec::new();
478        let _ = decoder.decode(data, &mut out);
479    }
480
481    /// An upstream MCP server's reply as the gateway reads it (#62): a JSON
482    /// body or an event stream, then the response's tools. Must never panic.
483    pub fn mcp_gateway_reply(data: &[u8]) {
484        for content_type in ["application/json", "text/event-stream"] {
485            if let Ok(result) = crate::mcp::gateway::parse_reply(content_type, data, 1) {
486                let _ = crate::mcp::gateway::fuzzing::tools(&result);
487            }
488        }
489    }
490
491    /// A WebSocket frame from a client (#8): any bytes; a frame never holds
492    /// more payload than the limit allows. Must never panic.
493    #[cfg(not(target_arch = "wasm32"))]
494    pub fn websocket_frame(data: &[u8]) {
495        let mut cursor = std::io::Cursor::new(data);
496        while let Ok(frame) = crate::websocket::WebSocket::read_frame_limited(&mut cursor, 4096) {
497            let len = match &frame {
498                crate::websocket::Frame::Text(t) => t.len(),
499                crate::websocket::Frame::Binary(b) | crate::websocket::Frame::Ping(b) | crate::websocket::Frame::Pong(b) => b.len(),
500                crate::websocket::Frame::Continuation { data, .. } => data.len(),
501                crate::websocket::Frame::Close(_, reason) => reason.len() + 2,
502            };
503            assert!(len <= 4096 + 2);
504        }
505    }
506
507    /// A DNS response read for SRV records (#8). Must never panic.
508    #[cfg(not(target_arch = "wasm32"))]
509    pub fn dns_srv_response(data: &[u8]) {
510        let id = data.get(..2).map_or(0, |b| u16::from_be_bytes([b[0], b[1]]));
511        let _ = crate::service_discovery::dns_srv::parse_response(data, id);
512    }
513
514    /// A DNS message: the shared parser (SRV discovery, ACME DNS-01), TXT
515    /// values and, with `acme`, a DNS UPDATE reply. Must never panic.
516    /// A certificate's subjectAltNames, as a BackendTLSPolicy checks a
517    /// backend's (untrusted) certificate. Must never panic.
518    pub fn x509_subject_alt_names(data: &[u8]) {
519        let _ = crate::core::x509::subject_alt_names(data);
520    }
521
522    pub fn dns_message(data: &[u8]) {
523        if let Ok(message) = crate::dns::parse(data) {
524            for record in message.answers.iter().chain(&message.authority).chain(&message.additional) {
525                let _ = crate::dns::txt_value(&data[record.rdata.clone()]);
526            }
527        }
528        #[cfg(feature = "acme")]
529        let _ = crate::acme::dns01::check_update_reply(data, 0);
530    }
531
532    /// A Redis (RESP) reply (#8). Must never panic.
533    #[cfg(not(target_arch = "wasm32"))]
534    pub fn redis_reply(data: &[u8]) {
535        let _ = crate::redis_protocol::read_reply(&mut std::io::Cursor::new(data));
536    }
537
538    /// A `Cookie` request header (#8): every parsed cookie has a name.
539    pub fn cookie_header(data: &[u8]) {
540        let Ok(value) = std::str::from_utf8(data) else { return };
541        let jar = crate::cookie::CookieJar::parse(value);
542        assert!(jar.cookies.iter().all(|c| !c.name.is_empty()));
543    }
544
545    /// A W3C `baggage` header (#25): members survive a round trip through
546    /// the header. Must never panic.
547    #[cfg(not(target_arch = "wasm32"))]
548    pub fn otel_baggage(data: &[u8]) {
549        let Ok(value) = std::str::from_utf8(data) else { return };
550        let baggage = crate::otel::Baggage::parse(value);
551        assert!(baggage.len() <= crate::otel::baggage::MAX_MEMBERS);
552        let again = crate::otel::Baggage::parse(&baggage.to_header());
553        for (key, value) in baggage.iter() {
554            if let Some(round_tripped) = again.get(key) {
555                assert_eq!(value, round_tripped);
556            }
557        }
558    }
559
560    /// What ACME renewal parses from the CA (#1): certificate DER (validity,
561    /// serial, Authority Key Identifier), renewalInfo windows (RFC 3339) and
562    /// directory profile names. Must never panic.
563    #[cfg(feature = "acme")]
564    pub fn acme_renewal(data: &[u8]) {
565        let _ = crate::acme::fuzzing::cert_info_from_der(data);
566        if let Ok(text) = std::str::from_utf8(data) {
567            let _ = crate::acme::fuzzing::parse_suggested_window(text);
568            let _ = crate::acme::fuzzing::parse_rfc3339(text);
569            let _ = crate::acme::fuzzing::json_object_keys(text, "profiles");
570        }
571    }
572
573    /// The SAML XML parser (`sso::saml::xml`), fed IdP-supplied documents.
574    #[cfg(feature = "sso-saml")]
575    pub fn saml_xml_parse(xml: &str) {
576        let _ = crate::sso::saml::xml_parse_for_fuzzing(xml);
577    }
578
579    /// HTTP/1.1 request framing (`server::framing`) over a raw connection
580    /// buffer: head detection, body framing, and the chunked body decoder,
581    /// fed in two pieces split at the first byte to mimic separate reads.
582    pub fn http1_framing(data: &[u8]) {
583        use crate::server::framing::{self, BodyFraming, ChunkedBody, HeadScan};
584        let mut pending = data.to_vec();
585        framing::skip_leading_empty_lines(&mut pending);
586        let HeadScan::Complete(head_len) = framing::scan_head(&pending) else { return };
587        assert!(head_len <= pending.len() && head_len <= framing::MAX_HEAD_SIZE);
588        let Ok(request) = crate::request::Request::parse(&pending[..head_len]) else { return };
589        for header in &request.headers {
590            let _ = crate::http::date::parse(&header.value);
591        }
592        let _ = framing::validate_head(&pending[..head_len], &request);
593        let _ = framing::keep_alive(&request);
594        let body = &pending[head_len..];
595        if let Ok(BodyFraming::Chunked) = framing::body_framing(&request) {
596            let split = data.first().map_or(0, |&b| b as usize).min(body.len());
597            let mut decoder = ChunkedBody::new(1 << 20);
598            let mut buffer = body[..split].to_vec();
599            for next in [&body[split..], &[][..]] {
600                match decoder.feed(&buffer) {
601                    Ok((consumed, _)) => {
602                        assert!(consumed <= buffer.len());
603                        buffer.drain(..consumed);
604                    }
605                    Err(_) => return,
606                }
607                buffer.extend_from_slice(next);
608            }
609            assert!(decoder.body.len() <= 1 << 20);
610        }
611    }
612}
613
614
615/// Compiles the README's Rust example as a doctest, so it can't drift from the API.
616#[cfg(all(doctest, feature = "http3"))]
617#[doc = include_str!("../README.md")]
618pub struct ReadmeDoctests;