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 = "18"
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//! ## Features
37//!
38//! On docs.rs every item is shown, labelled with the features it needs. The
39//! presets `web`, `api`, `proxy` and `full` turn on common sets; see
40//! [Feature flags](https://rws8.tech/getting-started/installation/#feature-flags).
41// docs.rs sets `--cfg docsrs` ([package.metadata.docs.rs]): nightly rustdoc
42// then labels each item with the `cfg` it is compiled under.
43#![cfg_attr(docsrs, feature(doc_cfg))]
44
45// Allows `::rust_web_server::…` paths to resolve from within this crate's own
46// tests, which is required by proc-macro derive output that uses that prefix.
47extern crate self as rust_web_server;
48
49pub use rws_server::app;
50#[cfg(feature = "auth")]
51pub use rws_server::auth;
52
53#[cfg(feature = "macros")]
54pub use rws_macros::{delete, get, patch, post, put, route, Config, FromPath, FromRequest, JsonSchema, Validate};
55#[cfg(all(feature = "macros", any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql")))]
56pub use rws_macros::{Factory, Model};
57#[cfg(feature = "http2")]
58pub use rws_server::async_state;
59#[cfg(feature = "http2")]
60pub(crate) use rws_server::async_bridge;
61pub use rws_server::session;
62// Socket-coupled — see spec/WASM_SHIM.md for what a wasm32-wasip2 build
63// excludes and why (no OS threads, no raw TCP/TLS in a WASI guest).
64#[cfg(not(target_arch = "wasm32"))]
65pub(crate) use rws_server::redis_protocol;
66pub use rws_server::sse;
67pub use rws_http::compression;
68pub use rws_server::cookie;
69pub use rws_http::error;
70pub use rws_http::extract;
71#[cfg(all(test, feature = "macros"))]
72mod extract_derive_tests;
73#[cfg(test)]
74mod http_tests;
75#[cfg(all(test, feature = "macros", feature = "model-sqlite"))]
76mod test_helper_tests;
77#[cfg(test)]
78mod server_tests;
79#[cfg(test)]
80mod feature_tests;
81pub mod i18n;
82pub use rws_proxy::a2a;
83#[cfg(all(unix, any(feature = "http1", feature = "http2")))]
84pub use rws_server::systemd;
85pub use rws_server::ip_filter;
86pub mod macros;
87pub use rws_server::blocklist;
88pub use rws_server::cache;
89pub use rws_server::early_hints;
90pub use rws_server::disk_guard;
91pub use rws_proxy::traffic;
92pub use rws_server::authz;
93pub use rws_server::config_reload;
94#[cfg(not(target_arch = "wasm32"))]
95pub use rws_server::config_watch;
96pub use rws_proxy::config_check;
97/// RSA public keys and RS256 / rsa-sha256 signature verification (no `rsa` crate).
98#[cfg(any(feature = "auth-asymmetric", feature = "sso"))]
99pub use rws_server::rsa_key;
100pub use rws_http::server_config;
101pub use rws_server::feature;
102pub use rws_server::maintenance;
103pub use rws_server::metrics;
104pub use rws_server::mcp;
105pub use rws_server::request_log;
106pub use rws_server::request_id;
107#[cfg(not(target_arch = "wasm32"))]
108pub use rws_server::otel;
109#[cfg(feature = "acme")]
110pub use rws_acme::acme;
111pub use rws_server::middleware;
112pub use rws_server::rate_limit;
113pub use rws_server::router;
114pub use rws_server::group;
115pub use rws_server::flash;
116#[cfg(not(target_arch = "wasm32"))]
117pub use rws_server::channels;
118pub use rws_server::events;
119pub use rws_server::health;
120pub use rws_server::error_pages;
121#[cfg(feature = "serde")]
122pub use rws_server::vite;
123pub use rws_server::handler;
124pub use rws_server::schema;
125pub use rws_server::state;
126pub use rws_server::test_client;
127pub use rws_server::application;
128/// `rws.app.toml` and the conventional app layout (18.28.0).
129pub use rws_http::app_manifest;
130pub use rws_http::body;
131pub use rws_http::client_hint;
132pub use rws_server::controller;
133pub use rws_base::core;
134pub use rws_http::cors;
135pub use rws_http::entry_point;
136pub use rws_base::ext;
137pub use rws_http::header;
138pub use rws_base::http;
139pub use rws_http::json;
140pub use rws_server::language;
141#[cfg(not(target_arch = "wasm32"))]
142pub use rws_server::log;
143pub use rws_base::mime_type;
144pub use rws_base::null;
145pub use rws_http::range;
146pub use rws_http::request;
147pub use rws_http::response;
148pub use rws_server::server;
149pub use rws_base::symbol;
150#[cfg(not(target_arch = "wasm32"))]
151pub use rws_server::thread_pool;
152pub use rws_base::url;
153pub use rws_server::pagination;
154#[cfg(not(target_arch = "wasm32"))]
155pub use rws_proxy::proxy;
156pub use rws_server::rewrite;
157pub use rws_server::scheduler;
158#[cfg(not(target_arch = "wasm32"))]
159pub use rws_proxy::tcp_proxy;
160#[cfg(not(target_arch = "wasm32"))]
161pub use rws_proxy::udp_proxy;
162#[cfg(not(target_arch = "wasm32"))]
163pub use rws_proxy::ws_proxy;
164#[cfg(not(target_arch = "wasm32"))]
165pub use rws_proxy::canary;
166#[cfg(not(target_arch = "wasm32"))]
167pub use rws_server::circuit_breaker;
168// `BackendPool`/`DiscoverySource` (DNS/etcd/Consul/Docker) are native-only —
169// gated internally in service_discovery/mod.rs, not at this declaration —
170// because `json_lite` (a plain JSON parser, no sockets) must stay available
171// for `secrets`'s Vault/AWS-SM/Key-Vault backends on every target.
172pub use rws_server::service_discovery;
173/// DNS wire format, shared by the SRV service discovery and ACME's DNS-01.
174pub(crate) use rws_base::dns;
175pub use rws_server::config_binding;
176pub use rws_server::di;
177#[cfg(not(target_arch = "wasm32"))]
178pub use rws_proxy::proxy_config;
179#[cfg(not(target_arch = "wasm32"))]
180pub use rws_proxy::llm;
181#[cfg(not(target_arch = "wasm32"))]
182pub use rws_proxy::admin;
183/// `app.dev_mcp()`: an MCP endpoint for a coding agent working on the app (18.28.0).
184#[cfg(not(target_arch = "wasm32"))]
185pub use rws_proxy::dev_mcp;
186/// `rws gen model|route|migration|test`: code generators for the
187/// conventional layout (18.29.0).
188#[cfg(not(target_arch = "wasm32"))]
189pub use rws_proxy::generator;
190/// `rws db migrate|rollback|status|seed`: the project's migrations and
191/// seeds from the command line (18.30.0).
192#[cfg(not(target_arch = "wasm32"))]
193pub use rws_proxy::db_cli;
194/// Fleet config pull: signed configs from HTTPS, S3 or git (18.23.0).
195#[cfg(all(feature = "http2", not(target_arch = "wasm32")))]
196pub use rws_proxy::fleet;
197/// Web application firewall: rws's own rules (18.22.0).
198pub use rws_proxy::waf;
199/// Runtime state shared across replicas through Redis (18.22.0).
200#[cfg(not(target_arch = "wasm32"))]
201pub use rws_proxy::cluster;
202/// FastCGI client: PHP through php-fpm (18.21.0).
203#[cfg(not(target_arch = "wasm32"))]
204pub use rws_proxy::fastcgi;
205/// `rws ctl` / `rwsctl`: the admin API from the command line (18.20.0).
206#[cfg(not(target_arch = "wasm32"))]
207pub mod ctl;
208#[cfg(feature = "wasm-plugins")]
209pub use rws_wasm_plugin::wasm_plugin;
210#[cfg(not(target_arch = "wasm32"))]
211pub use rws_proxy::ingress;
212#[cfg(feature = "tera")]
213pub mod template;
214pub use rws_server::validate;
215pub use rws_http::virtual_host;
216#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
217pub use rws_server::model;
218#[cfg(not(target_arch = "wasm32"))]
219pub use rws_server::websocket;
220// Has both a native (TcpStream/rustls) and a wasm32 (wasi:http outgoing-handler)
221// backend behind Client::send() — see the #[cfg(target_arch = "wasm32")]
222// `send_once` in this module. spec/WASM_SHIM.md Phase 2 item 7.
223pub use rws_server::http_client;
224#[cfg(feature = "crypto")]
225pub use rws_server::crypto;
226#[cfg(feature = "csrf")]
227pub use rws_server::csrf;
228#[cfg(feature = "sso")]
229pub use rws_server::sso;
230// SMTP is a raw-TCP protocol with no wasi:http equivalent (unlike
231// http_client, which the host can proxy via outgoing-handler) — genuinely
232// can't work in a wasi:http guest, not just ungated. spec/WASM_SHIM.md.
233#[cfg(all(feature = "mailer", not(target_arch = "wasm32")))]
234pub mod mailer;
235#[cfg(all(feature = "crypto", feature = "mailer", feature = "serde", not(target_arch = "wasm32")))]
236pub mod auth_flows;
237#[cfg(all(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"), not(target_arch = "wasm32")))]
238pub mod model_admin;
239#[cfg(feature = "jobs")]
240pub mod jobs;
241// Also compiled under `secrets` — `secrets::aws_secrets_manager`/`azure_key_vault`
242// reuse `StorageError` and the SigV4/Managed-Identity credential helpers from here.
243#[cfg(any(feature = "storage-local", feature = "storage-s3", feature = "storage-azure", feature = "storage-gcs"))]
244pub use rws_storage::storage;
245#[cfg(any(feature = "storage-local", feature = "storage-s3", feature = "storage-azure", feature = "storage-gcs"))]
246pub mod attachment;
247// `secrets` alone needs only what rws-server's slim `storage` has.
248#[cfg(all(feature = "secrets", not(any(feature = "storage-local", feature = "storage-s3", feature = "storage-azure", feature = "storage-gcs"))))]
249pub use rws_server::storage;
250#[cfg(feature = "openapi")]
251pub use rws_server::openapi;
252#[cfg(feature = "tower")]
253pub use rws_tower::tower;
254#[cfg(feature = "gateway-api")]
255pub use rws_gateway_api::gateway_api;
256/// Kubernetes integration and operator framework (`k8s` feature, 18.35.0).
257#[cfg(feature = "k8s")]
258pub use rws_k8s::k8s;
259#[cfg(feature = "webhook")]
260pub mod webhook;
261#[cfg(feature = "secrets")]
262pub use rws_server::secrets;
263// Runs wrapped work on a background OS thread to bound wait time — needs
264// `std::thread::spawn`, unavailable on wasm32-wasip2. See spec/WASM_SHIM.md.
265#[cfg(not(target_arch = "wasm32"))]
266pub use rws_server::timeout;
267pub mod prelude;
268
269#[cfg(feature = "http2")]
270#[doc(hidden)]
271pub use rws_server::tls;
272
273#[cfg(feature = "http2")]
274#[doc(hidden)]
275pub use rws_server::h2_server;
276
277#[cfg(feature = "http3")]
278#[doc(hidden)]
279pub use rws_server::h3_handler;
280
281/// Shared infrastructure for tests that write process-wide environment variables.
282///
283/// Tests that call `override_environment_variables_from_config` or
284/// `CommandLineArgument::set_environment_variable` must hold this lock for
285/// their entire duration so they don't race with other tests reading the
286/// same variables.
287#[cfg(test)]
288mod stack_depth_tests;
289
290// The rustls crypto provider and client builder live in rws-server (its TLS
291// code uses them); `FIPS` too.
292#[cfg(all(any(feature = "http-client", feature = "http2"), not(target_arch = "wasm32")))]
293pub use rws_server::crypto_provider;
294pub use rws_server::FIPS;
295#[cfg(all(any(feature = "http-client", feature = "http2"), not(target_arch = "wasm32")))]
296pub(crate) use rws_server::tls_client_builder;
297
298#[cfg(test)]
299pub mod test_env {
300    use std::sync::{Mutex, OnceLock};
301    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
302    pub fn lock() -> std::sync::MutexGuard<'static, ()> {
303        LOCK.get_or_init(|| Mutex::new(()))
304            .lock()
305            .unwrap_or_else(|e| e.into_inner())
306    }
307
308    /// A per-process path under the OS temp dir that is deleted (file or
309    /// directory) when dropped — so a test cleans up even when an assertion
310    /// fails before its last line.
311    pub struct TempPath(std::path::PathBuf);
312
313    impl TempPath {
314        pub fn new(name: &str) -> Self {
315            TempPath(std::env::temp_dir().join(format!("rws_test_{}_{}", std::process::id(), name)))
316        }
317
318        /// The same in the working directory (the static-file root), for
319        /// tests that need a file the server would serve.
320        pub fn new_in_cwd(name: &str) -> Self {
321            TempPath(std::env::current_dir().expect("working directory").join(format!("rws_test_{}_{}", std::process::id(), name)))
322        }
323
324        pub fn as_str(&self) -> &str {
325            self.0.to_str().expect("temp dir path is UTF-8")
326        }
327    }
328
329    impl std::ops::Deref for TempPath {
330        type Target = std::path::Path;
331        fn deref(&self) -> &std::path::Path {
332            &self.0
333        }
334    }
335
336    impl Drop for TempPath {
337        fn drop(&mut self) {
338            // A symlink is removed itself, never followed (a test may link
339            // to a system directory).
340            let is_link = std::fs::symlink_metadata(&self.0).is_ok_and(|m| m.file_type().is_symlink());
341            if is_link {
342                let _ = std::fs::remove_file(&self.0);
343            } else if self.0.is_dir() {
344                let _ = std::fs::remove_dir_all(&self.0);
345            } else {
346                let _ = std::fs::remove_file(&self.0);
347            }
348        }
349    }
350}
351
352/// Entry points for `fuzz/` targets that reach crate-private parsers.
353/// Compiled only under `--cfg fuzzing`, which `cargo fuzz` sets — never part
354/// of the public API.
355#[cfg(fuzzing)]
356#[doc(hidden)]
357pub mod fuzz_api {
358    /// A fleet signature file (`rws-fleet-sig v2 <serial> <sig>` or the
359    /// 18.23 form) checked against a body (18.24.0). Must never panic or
360    /// accept: no input is a valid signature for the fixed key.
361    #[cfg(all(feature = "http2", not(target_arch = "wasm32")))]
362    pub fn fleet_signature(data: &[u8]) {
363        let _ = crate::fleet::parse_signature(data);
364        let (body, sig) = data.split_at(data.len() / 2);
365        assert!(crate::fleet::verify(body, sig, &[7u8; 32]).is_err(), "a fuzzed signature verified");
366    }
367
368    /// The shared base64 decoder (18.25.0), every alphabet and option:
369    /// never panics, and whatever decodes re-encodes to the same bytes.
370    pub fn base64_decode(data: &[u8]) {
371        use crate::core::base64::{Base64, Decoder};
372        for d in [Decoder::STANDARD, Decoder::URL_SAFE, Decoder::EITHER.skip_whitespace(), Decoder::EITHER.skip_invalid()] {
373            if let Ok(bytes) = d.decode(data) {
374                let again = Decoder::STANDARD.decode(Base64::encode(&bytes).unwrap()).unwrap();
375                assert_eq!(bytes, again, "base64 round trip changed the bytes");
376            }
377        }
378    }
379
380    /// An RFC 7512 `pkcs11:` URI (18.24.0). Must never panic.
381    #[cfg(feature = "pkcs11")]
382    pub fn pkcs11_uri(data: &[u8]) {
383        if let Ok(text) = std::str::from_utf8(data) {
384            let _ = crate::tls::pkcs11::Pkcs11Uri::parse(text);
385            let _ = crate::entry_point::shown("RWS_CONFIG_TLS_KEY_FILE", text);
386        }
387    }
388
389    /// Proxy route-matching normalization of a request target (#50): any
390    /// input, and normalizing twice changes nothing.
391    pub fn proxy_normalize_target(data: &[u8]) {
392        let Ok(target) = std::str::from_utf8(data) else { return };
393        let once = crate::proxy_config::normalize_request_target(target).into_owned();
394        let twice = crate::proxy_config::normalize_request_target(&once).into_owned();
395        assert_eq!(once, twice, "normalization is not idempotent for {:?}", target);
396    }
397
398    /// Static-file request-path decoding (#55). Must never panic; the
399    /// decoded path never carries a `..` segment past the web-root check.
400    pub fn static_path_decode(data: &[u8]) {
401        let Ok(path) = std::str::from_utf8(data) else { return };
402        if let Ok(fs_path) = crate::app::controller::static_resource::static_fs_path(path) {
403            let relative = &fs_path[crate::app::controller::static_resource::working_directory().len()..];
404            assert!(!relative.split(['/', '\\']).any(|segment| segment == ".."), "{:?} -> {:?}", path, fs_path);
405        }
406    }
407
408    /// OpenAPI documents to MCP tools (#66): any bytes. Must never panic;
409    /// every tool's schemas are JSON and its requests build or err cleanly.
410    pub fn openapi_to_mcp(data: &[u8]) {
411        let Ok(spec) = std::str::from_utf8(data) else { return };
412        let options = crate::mcp::openapi_tools::ToolOptions::default();
413        let Ok(found) = crate::mcp::openapi_tools::tools(spec, &options) else { return };
414        for tool in found {
415            assert!(crate::service_discovery::json_lite::parse(&tool.input_schema).is_ok());
416            assert!(tool.name.len() <= 64 && tool.name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-'), "{}", tool.name);
417            let _ = tool.request(r#"{"id":"1","body":{"a":[1,2]},"q":["x","y"]}"#);
418        }
419    }
420
421    /// An A2A JSON-RPC request body (#65): any bytes. Must never panic, and
422    /// the answer is always a JSON-RPC response (or a 413).
423    pub fn a2a_request(data: &[u8]) {
424        use crate::application::Application;
425        thread_local! {
426            static AGENT: crate::a2a::A2aServer = crate::a2a::A2aServer::new(crate::a2a::AgentCard::new("fuzz", "fuzz", "1"))
427                .on_message(|ctx, message| match message.text().len() % 3 {
428                    0 => crate::a2a::A2aReply::Message(crate::a2a::Message::agent_text(message.text())),
429                    1 => crate::a2a::A2aReply::Task(ctx.input_required("more?")),
430                    _ => crate::a2a::A2aReply::Task(ctx.complete(vec![crate::a2a::Artifact::text("a", message.text())])),
431                });
432        }
433        let request = crate::request::Request {
434            method: "POST".to_string(),
435            request_uri: "/a2a".to_string(),
436            http_version: "HTTP/1.1".to_string(),
437            headers: vec![crate::header::Header::new("A2A-Version", "1.0".to_string())],
438            body: data.to_vec().into(),
439        };
440        let connection = crate::server::ConnectionInfo::default();
441        let response = AGENT.with(|agent| agent.execute(&request, &connection)).expect("A2A never errs");
442        if response.status_code == 200 {
443            let body = String::from_utf8(response.content_range_list[0].body.clone()).expect("UTF-8");
444            assert!(crate::service_discovery::json_lite::parse(&body).is_ok(), "not JSON: {body}");
445        }
446    }
447
448    /// `Accept-Language` parsing and negotiation (#33): any header value;
449    /// at most `MAX_LANGUAGE_RANGES` ranges, and the language chosen is
450    /// always one of those offered.
451    pub fn accept_language(data: &[u8]) {
452        let Ok(value) = std::str::from_utf8(data) else { return };
453        let preferences = crate::i18n::AcceptLanguage::parse(value);
454        assert!(preferences.0.len() <= crate::i18n::MAX_LANGUAGE_RANGES);
455        let available = ["en", "en-GB", "uk", "pt-BR"];
456        if let Some(chosen) = preferences.negotiate(&available) {
457            assert!(available.contains(&chosen));
458        }
459    }
460
461    /// i18n catalog text (#33): any input; whatever parses translates
462    /// without panicking, plurals and placeholders included.
463    pub fn i18n_catalog(data: &[u8]) {
464        let Ok(text) = std::str::from_utf8(data) else { return };
465        if let Ok(catalogs) = crate::i18n::Catalogs::new("en").add("en", text) {
466            let t = catalogs.for_language("en");
467            for line in text.lines() {
468                let key = line.split('=').next().unwrap_or("").trim();
469                let _ = t.t_with(key, &[("name", "x")]);
470                let _ = t.plural(key.trim_end_matches(".one").trim_end_matches(".other"), 3);
471            }
472        }
473    }
474
475    /// `Accept-Encoding` negotiation (#7): any header value; the choice is
476    /// always one of the offered codings, and a coding the value refuses
477    /// (its first `q=` parameter is 0) is never chosen.
478    pub fn accept_encoding(data: &[u8]) {
479        let Ok(value) = std::str::from_utf8(data) else { return };
480        let request = crate::request::Request {
481            method: "GET".to_string(),
482            request_uri: "/".to_string(),
483            http_version: "HTTP/1.1".to_string(),
484            headers: vec![crate::header::Header::new(crate::header::Header::_ACCEPT_ENCODING, value.to_string())],
485            body: Default::default(),
486        };
487        for preference in [crate::compression::DYNAMIC_PREFERENCE, crate::compression::STATIC_PREFERENCE] {
488            if let Some(chosen) = crate::compression::negotiate(&request, preference) {
489                assert!(preference.contains(&chosen));
490                // Refused: the coding's first `q=` parameter is exactly 0.
491                let refused = value.split(',').any(|c| {
492                    let mut parts = c.split(';');
493                    parts.next().unwrap_or("").trim().eq_ignore_ascii_case(chosen.token())
494                        && parts
495                            .map(str::trim)
496                            .find(|p| p.as_bytes().get(..2).is_some_and(|b| b.eq_ignore_ascii_case(b"q=")))
497                            .is_some_and(|q| q[2..].trim().parse::<f32>() == Ok(0.0))
498                });
499                let listed_once = value.split(',').filter(|c| c.split(';').next().unwrap_or("").trim().eq_ignore_ascii_case(chosen.token())).count() == 1;
500                assert!(!(refused && listed_once), "{value:?} refused {chosen:?}");
501            }
502        }
503    }
504
505    /// What a WebAssembly plugin returns, as the host reads it (#40).
506    /// Must never panic.
507    #[cfg(feature = "wasm-plugins")]
508    pub fn wasm_plugin_output(data: &[u8]) {
509        let _ = crate::wasm_plugin::parse_request_output(data);
510        let _ = crate::wasm_plugin::parse_response_output(data);
511    }
512
513    /// A disk-cache entry file as the cache reads it back (#39): the header
514    /// line and key, then the stored response. Must never panic.
515    pub fn cache_entry(data: &[u8]) {
516        if let Some((_, _, _, offset)) = crate::cache::disk::parse_entry_header(data) {
517            let _ = crate::response::Response::parse_upstream(&data[offset..], crate::response::REGENERATED_OR_HOP_BY_HOP);
518        }
519    }
520
521    /// A request as the traffic classifier sees it: the first line is the
522    /// User-Agent, the rest the path; the address comes from the first byte.
523    pub fn traffic_classify(data: &[u8]) {
524        let text = String::from_utf8_lossy(data);
525        let (ua, path) = text.split_once('\n').unwrap_or((&text, "/"));
526        let ip = std::net::IpAddr::from([data.first().copied().unwrap_or(1), 2, 3, 4]);
527        let class = crate::traffic::classify(ip, Some(ua), path);
528        let _ = (class.group(), class.kind.label());
529        // The audience and source parsers (18.31) see the same input.
530        let _ = crate::traffic::device::parse(ua, Some(path), Some(ua));
531        let _ = crate::traffic::record::utm(path);
532        let _ = crate::traffic::record::channel_of(ua);
533        // A script report (18.31): parsed only once counting is on.
534        crate::traffic::enable();
535        crate::traffic::record::beacon(ip, "fuzz.example", Some(ua), path, 0);
536        // Apps (18.35): the User-Agent with the second line as an Origin,
537        // and the second line as an app's event (`{"app":"Fuzz",…}` is taken).
538        let _ = crate::traffic::apps::detect(Some(ua), Some(path));
539        if !crate::traffic::apps::any_configured() {
540            crate::traffic::apps::set_apps(&["Fuzz=fuzz/".to_string()]);
541        }
542        let _ = crate::traffic::record::app_event(ip, "fuzz.example", Some(ua), path, 0);
543    }
544
545    /// A country table as `[traffic] geo_db` loads it (18.31): inflated and
546    /// raw. Must never panic, nor allocate without bound.
547    pub fn traffic_geo(data: &[u8]) {
548        for table in [crate::traffic::geo::Table::parse(data), crate::traffic::geo::Table::decode(data)].into_iter().flatten() {
549            let _ = table.country(std::net::IpAddr::from([data.first().copied().unwrap_or(1), 2, 3, 4]));
550            let _ = table.country("2001:db8::1".parse().unwrap());
551        }
552    }
553
554    /// Authorization text as the admin API and config file read it (#32):
555    /// a tuple, a subject, and a relation definition.
556    pub fn authz_parse(data: &[u8]) {
557        let text = String::from_utf8_lossy(data);
558        let _ = crate::authz::Tuple::parse(&text);
559        let _ = crate::authz::Subject::parse(&text);
560        if let Ok(terms) = crate::authz::rebac::parse_expression(&text) {
561            let _ = terms.len();
562            let _ = crate::authz::Schema::new().relation("t", "r", &text).and_then(|s| s.validate());
563        }
564    }
565
566    /// An `Available-Dictionary` header value, and dictionary-compressed
567    /// bodies through both decoders (#38).
568    pub fn available_dictionary(data: &[u8]) {
569        use crate::compression::dictionary::{decode, parse_available_dictionary, DictionaryEncoding};
570        let _ = parse_available_dictionary(&String::from_utf8_lossy(data));
571        for encoding in [DictionaryEncoding::Dcz, DictionaryEncoding::Dcb] {
572            let _ = decode(encoding, data, b"dictionary", 1 << 16);
573        }
574    }
575
576    /// Provider responses as the LLM proxy reads them (#41): usage from a
577    /// JSON body or an event stream fed in pieces, and chunked framing.
578    /// Must never panic.
579    pub fn llm_usage(data: &[u8]) {
580        for content_type in ["application/json", "text/event-stream"] {
581            let mut parser = crate::llm::UsageParser::new(content_type);
582            for piece in data.chunks(7) {
583                parser.feed(piece);
584            }
585            let _ = parser.finish();
586        }
587        let mut decoder = crate::llm::usage::ChunkDecoder::default();
588        let mut out = Vec::new();
589        let _ = decoder.decode(data, &mut out);
590    }
591
592    /// An upstream MCP server's reply as the gateway reads it (#62): a JSON
593    /// body or an event stream, then the response's tools. Must never panic.
594    pub fn mcp_gateway_reply(data: &[u8]) {
595        for content_type in ["application/json", "text/event-stream"] {
596            if let Ok(result) = crate::mcp::gateway::parse_reply(content_type, data, 1) {
597                let _ = crate::mcp::gateway::fuzzing::tools(&result);
598            }
599        }
600    }
601
602    /// A WebSocket frame from a client (#8): any bytes; a frame never holds
603    /// more payload than the limit allows. Must never panic.
604    #[cfg(not(target_arch = "wasm32"))]
605    pub fn websocket_frame(data: &[u8]) {
606        let mut cursor = std::io::Cursor::new(data);
607        while let Ok(frame) = crate::websocket::WebSocket::read_frame_limited(&mut cursor, 4096) {
608            let len = match &frame {
609                crate::websocket::Frame::Text(t) => t.len(),
610                crate::websocket::Frame::Binary(b) | crate::websocket::Frame::Ping(b) | crate::websocket::Frame::Pong(b) => b.len(),
611                crate::websocket::Frame::Continuation { data, .. } => data.len(),
612                crate::websocket::Frame::Close(_, reason) => reason.len() + 2,
613            };
614            assert!(len <= 4096 + 2);
615        }
616    }
617
618    /// A DNS response read for SRV records (#8). Must never panic.
619    #[cfg(not(target_arch = "wasm32"))]
620    pub fn dns_srv_response(data: &[u8]) {
621        let id = data.get(..2).map_or(0, |b| u16::from_be_bytes([b[0], b[1]]));
622        let _ = crate::service_discovery::dns_srv::parse_response(data, id);
623    }
624
625    /// A DNS message: the shared parser (SRV discovery, ACME DNS-01), TXT
626    /// values and, with `acme`, a DNS UPDATE reply. Must never panic.
627    /// A certificate's subjectAltNames, as a BackendTLSPolicy checks a
628    /// backend's (untrusted) certificate. Must never panic.
629    pub fn x509_subject_alt_names(data: &[u8]) {
630        let _ = crate::core::x509::subject_alt_names(data);
631    }
632
633    pub fn dns_message(data: &[u8]) {
634        if let Ok(message) = crate::dns::parse(data) {
635            for record in message.answers.iter().chain(&message.authority).chain(&message.additional) {
636                let _ = crate::dns::txt_value(&data[record.rdata.clone()]);
637            }
638        }
639        #[cfg(feature = "acme")]
640        let _ = crate::acme::dns01::check_update_reply(data, 0);
641    }
642
643    /// A Redis (RESP) reply (#8). Must never panic.
644    #[cfg(not(target_arch = "wasm32"))]
645    pub fn redis_reply(data: &[u8]) {
646        let _ = crate::redis_protocol::read_reply(&mut std::io::Cursor::new(data));
647    }
648
649    /// A `Cookie` request header (#8): every parsed cookie has a name.
650    pub fn cookie_header(data: &[u8]) {
651        let Ok(value) = std::str::from_utf8(data) else { return };
652        let jar = crate::cookie::CookieJar::parse(value);
653        assert!(jar.cookies.iter().all(|c| !c.name.is_empty()));
654    }
655
656    /// A W3C `baggage` header (#25): members survive a round trip through
657    /// the header. Must never panic.
658    #[cfg(not(target_arch = "wasm32"))]
659    pub fn otel_baggage(data: &[u8]) {
660        let Ok(value) = std::str::from_utf8(data) else { return };
661        let baggage = crate::otel::Baggage::parse(value);
662        assert!(baggage.len() <= crate::otel::baggage::MAX_MEMBERS);
663        let again = crate::otel::Baggage::parse(&baggage.to_header());
664        for (key, value) in baggage.iter() {
665            if let Some(round_tripped) = again.get(key) {
666                assert_eq!(value, round_tripped);
667            }
668        }
669    }
670
671    /// What ACME renewal parses from the CA (#1): certificate DER (validity,
672    /// serial, Authority Key Identifier), renewalInfo windows (RFC 3339) and
673    /// directory profile names. Must never panic.
674    #[cfg(feature = "acme")]
675    pub fn acme_renewal(data: &[u8]) {
676        let _ = crate::acme::fuzzing::cert_info_from_der(data);
677        if let Ok(text) = std::str::from_utf8(data) {
678            let _ = crate::acme::fuzzing::parse_suggested_window(text);
679            let _ = crate::acme::fuzzing::parse_rfc3339(text);
680            let _ = crate::acme::fuzzing::json_object_keys(text, "profiles");
681        }
682    }
683
684    /// The SAML XML parser (`sso::saml::xml`), fed IdP-supplied documents.
685    #[cfg(feature = "sso-saml")]
686    pub fn saml_xml_parse(xml: &str) {
687        let _ = crate::sso::saml::xml_parse_for_fuzzing(xml);
688    }
689
690    /// HTTP/1.1 request framing (`server::framing`) over a raw connection
691    /// buffer: head detection, body framing, and the chunked body decoder,
692    /// fed in two pieces split at the first byte to mimic separate reads.
693    pub fn http1_framing(data: &[u8]) {
694        use crate::server::framing::{self, BodyFraming, ChunkedBody, HeadScan};
695        let mut pending = data.to_vec();
696        framing::skip_leading_empty_lines(&mut pending);
697        let HeadScan::Complete(head_len) = framing::scan_head(&pending) else { return };
698        assert!(head_len <= pending.len() && head_len <= framing::MAX_HEAD_SIZE);
699        let Ok(request) = crate::request::Request::parse(&pending[..head_len]) else { return };
700        for header in &request.headers {
701            let _ = crate::http::date::parse(&header.value);
702        }
703        let _ = framing::validate_head(&pending[..head_len], &request);
704        let _ = framing::keep_alive(&request);
705        let body = &pending[head_len..];
706        if let Ok(BodyFraming::Chunked) = framing::body_framing(&request) {
707            let split = data.first().map_or(0, |&b| b as usize).min(body.len());
708            let mut decoder = ChunkedBody::new(1 << 20);
709            let mut buffer = body[..split].to_vec();
710            for next in [&body[split..], &[][..]] {
711                match decoder.feed(&buffer) {
712                    Ok((consumed, _)) => {
713                        assert!(consumed <= buffer.len());
714                        buffer.drain(..consumed);
715                    }
716                    Err(_) => return,
717                }
718                buffer.extend_from_slice(next);
719            }
720            assert!(decoder.body.len() <= 1 << 20);
721        }
722    }
723}
724
725
726/// Compiles the README's Rust example as a doctest, so it can't drift from the API.
727#[cfg(all(doctest, feature = "http3"))]
728#[doc = include_str!("../README.md")]
729pub struct ReadmeDoctests;