rws-server 18.35.1

The server layer of rust-web-server (internal: use the rust-web-server crate).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
#[cfg(test)]
mod tests;

pub mod controller;

use std::sync::Arc;

use crate::app::controller::directory_listing::DirectoryListingAssetsController;
use crate::app::controller::favicon::FaviconController;
use crate::app::controller::health::HealthController;
use crate::app::controller::ready::ReadyController;
use crate::app::controller::metrics::MetricsController;
use crate::app::controller::file::initiate::FileUploadInitiateController;
use crate::app::controller::form::get_method::FormGetMethodController;
use crate::app::controller::form::multipart_enctype_post_method::FormMultipartEnctypePostMethodController;
use crate::app::controller::form::url_encoded_enctype_post_method::FormUrlEncodedEnctypePostMethodController;
use crate::app::controller::index::IndexController;
use crate::app::controller::not_found::NotFoundController;
use crate::app::controller::script::ScriptController;
use crate::app::controller::static_resource::StaticResourceController;
use crate::app::controller::style::StyleController;
use crate::application::Application;
use crate::controller::Controller;
use crate::core::New;
use crate::header::Header;
use crate::mcp::McpServer;
use crate::middleware::{Middleware, WithMiddleware};
use crate::request::Request;
use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
use crate::server::ConnectionInfo;
use crate::server_config::ServerConfig;
use crate::state::AppWithState;

/// A pair of function pointers representing one entry in the controller chain.
struct ControllerEntry {
    is_matching: fn(&Request, &ConnectionInfo) -> bool,
    process: fn(&Request, Response, &ConnectionInfo) -> Response,
}

/// Build a [`ControllerEntry`] from any type that implements [`Controller`].
fn entry<C: Controller>() -> ControllerEntry {
    ControllerEntry {
        is_matching: C::is_matching,
        process: C::process,
    }
}

/// The built-in HTTP application. Serves static files, favicons, forms,
/// file uploads, health probes, metrics, and a 404 fallback.
///
/// Use as-is or compose with the framework's building blocks:
///
/// ```rust,no_run
/// # extern crate rws_server as rust_web_server;
/// use rust_web_server::app::App;
/// use rust_web_server::middleware::{WithMiddleware, RateLimitLayer};
/// use rust_web_server::core::New;
///
/// // Middleware stack around the built-in app
/// let app = App::new().wrap(RateLimitLayer);
/// ```
///
/// For user-defined routes with shared state, call [`App::with_state`]:
///
/// ```rust,no_run
/// # extern crate rws_server as rust_web_server;
/// use rust_web_server::app::App;
/// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
/// use rust_web_server::core::New;
///
/// struct State { version: &'static str }
///
/// let app = App::with_state(State { version: "1.0" })
///     .get("/version", |_req, _params, _conn, state| {
///         let mut r = Response::new();
///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
///         r
///     });
/// ```
///
/// # Per-instance configuration
///
/// By default `App::new()` reads CORS, CSP, and other settings from
/// environment variables on each request (matching the current process state,
/// including hot-reloaded values). Call [`App::with_config`] to pin the
/// configuration to a specific [`ServerConfig`] — this is the recommended
/// pattern for parallel integration tests, which should not touch environment
/// variables at all.
///
/// ```rust,ignore
/// # extern crate rws_server as rust_web_server;
/// use rust_web_server::app::App;
/// use rust_web_server::server_config::ServerConfig;
/// use rust_web_server::test_client::TestClient;
///
/// // No env writes, no lock needed — safe to run in parallel.
/// let app = App::with_config(ServerConfig {
///     cors_allow_all: false,
///     cors_allow_origins: "https://trusted.example.com".to_string(),
///     ..ServerConfig::default()
/// });
/// let client = TestClient::new(app);
/// ```
#[derive(Clone)]
pub struct App {
    /// When `Some`, every request is served using this fixed config.
    /// When `None`, config is read from environment variables on each request
    /// (supports hot-reload via SIGHUP / `POST /admin/config/reload`).
    config: Option<Arc<ServerConfig>>,
}

impl New for App {
    fn new() -> Self {
        App { config: None }
    }
}

impl App {
    /// Create an `App` pinned to an explicit [`ServerConfig`].
    ///
    /// All CORS, CSP, and other header settings are taken from `config` rather
    /// than from environment variables. The configuration is fixed for the
    /// lifetime of the `App` instance — SIGHUP / hot-reload do not affect it.
    ///
    /// This is the preferred constructor for integration tests: build a
    /// [`ServerConfig`] with the exact settings under test, pass it here, and
    /// use [`TestClient`] to drive requests. No environment writes and no
    /// [`test_env::lock()`] are needed.
    ///
    /// [`TestClient`]: crate::test_client::TestClient
    /// [`test_env::lock()`]: crate::test_env::lock
    pub fn with_config(config: ServerConfig) -> Self {
        App { config: Some(Arc::new(config)) }
    }

    /// [`App::with_config`] sharing an existing `Arc` instead of copying it.
    #[doc(hidden)]
    pub fn with_shared_config(config: Arc<ServerConfig>) -> Self {
        App { config: Some(config) }
    }

    /// Whether the built-in controllers answer `request` without blocking:
    /// every GET/HEAD they serve is in memory or a local file read (health,
    /// metrics, assets, static files — large files are only opened here and
    /// streamed from the blocking pool). Uploads and form posts aren't.
    #[doc(hidden)]
    pub fn is_inline_request(request: &Request) -> bool {
        let read = request.method == crate::request::METHOD.get || request.method == crate::request::METHOD.head;
        // A probe whose checks may block (a database ping) runs off the event loop.
        read && !App::probe_request(request).is_some_and(|(probe, _)| crate::health::has_checks(probe))
    }

    /// The probe the `health` registry answers for `request`, and whether
    /// `?verbose` was asked for.
    fn probe_request(request: &Request) -> Option<(crate::health::Probe, bool)> {
        if request.method != crate::request::METHOD.get && request.method != crate::request::METHOD.head {
            return None;
        }
        let (path, query) = request.request_uri.split_once('?').unwrap_or((&request.request_uri, ""));
        let probe = crate::health::probe_for_path(path)?;
        let verbose = query.split('&').any(|p| p == "verbose" || p.starts_with("verbose="));
        let legacy = path == "/healthz" || path == "/readyz";
        (!legacy || verbose || crate::health::has_checks(probe)).then_some((probe, verbose))
    }
}

impl Application for App {
    // `App` dispatches through a fixed if-chain of built-in `Controller`s
    // rather than a `Router`: the built-in set is small, static, and known
    // at compile time, so a linear scan is simpler and just as fast as a
    // segment-matching router would be here. This is a deliberate choice,
    // not an oversight — `Router` (src/router/mod.rs) is the right tool for
    // user-defined routes with named path params/wildcards, and `AppWithState`
    // / `AsyncAppWithState` use exactly that, falling through to this same
    // controller chain for anything they don't handle themselves.
    fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
        let config = match &self.config {
            Some(c) => Arc::clone(c),
            None => ServerConfig::current(),
        };
        let header_list = Header::get_header_list_with_config(request, &config);
        let response = crate::error::problem::with_headers(
            STATUS_CODE_REASON_PHRASE.n501_not_implemented,
            &format!("no handler for method {}", request.method),
            "",
            Some(header_list),
        );

        // Probes with registered checks, `?verbose`, `/livez`, `/startupz`
        // (18.35.0): the `health` registry answers. Without checks
        // `/healthz` and `/readyz` keep their controllers' answers.
        if let Some((probe, verbose)) = App::probe_request(request) {
            return Ok(crate::health::respond(probe, verbose));
        }

        let controllers = [
            entry::<IndexController>(),
            entry::<StyleController>(),
            entry::<ScriptController>(),
            entry::<DirectoryListingAssetsController>(),
            entry::<FileUploadInitiateController>(),
            entry::<FormUrlEncodedEnctypePostMethodController>(),
            entry::<FormGetMethodController>(),
            entry::<FormMultipartEnctypePostMethodController>(),
            entry::<HealthController>(),
            entry::<ReadyController>(),
            entry::<MetricsController>(),
            entry::<FaviconController>(),
        ];

        for c in &controllers {
            if (c.is_matching)(request, connection) {
                let mut response = (c.process)(request, response, connection);
                // The site's own index.html is a static file: the static
                // Cache-Control (default `no-cache`) instead of the dynamic
                // `no-store`, which kept browsers from caching the home page
                // at all (and out of their back/forward cache).
                if IndexController::is_matching(request, connection)
                    && response.status_code == *STATUS_CODE_REASON_PHRASE.n200_ok.status_code
                    && file_ext::FileExt::does_file_exist(IndexController::INDEX_FILEPATH)
                {
                    crate::app::controller::static_resource::set_static_cache_control(&mut response, &config.static_cache_control);
                }
                return Ok(response);
            }
        }

        // Static files, after every built-in route. A GET/HEAD of a regular
        // file is answered in one pass — StaticResourceController::is_matching
        // would build the path and look the file up, only for process to do
        // both again; serving one implies is_matching would have said yes.
        let mut response = response;
        if let Some(served) = StaticResourceController::serve_regular_file(request, &mut response, &config) {
            return Ok(served);
        }
        if StaticResourceController::is_matching(request, connection)
            && (config.directory_listing || !StaticResourceController::is_directory_without_index(request))
        {
            return Ok(StaticResourceController::process_general(request, response, &config));
        }
        // `/docs/http2/):` — a link copied with the prose around it.
        if let Some(redirect) = StaticResourceController::trailing_punctuation_redirect(request) {
            return Ok(redirect);
        }
        if NotFoundController::is_matching(request, connection) {
            return Ok(NotFoundController::process(request, response, connection));
        }

        Ok(response)
    }

    fn runs_inline(&self, request: &Request, _connection: &ConnectionInfo) -> bool {
        App::is_inline_request(request)
    }
}

impl App {
    /// Dispatch `request` through the controller chain and return the response.
    ///
    /// This is a convenience wrapper over [`Application::execute`] that uses a
    /// synthetic loopback [`ConnectionInfo`]. Use it in tests or when no real
    /// connection context is available. Prefer [`TestClient`] for structured
    /// test code.
    ///
    /// [`TestClient`]: crate::test_client::TestClient
    pub fn handle_request(request: Request) -> (Response, Request) {
        use crate::server::Address;
        let conn = ConnectionInfo {
            client: Address { ip: "127.0.0.1".to_string(), port: 0 },
            server: Address { ip: "127.0.0.1".to_string(), port: 7878 },
            request_size: 16000,
            sni_hostname: None,
            tls: false,
        };
        let app = App::new();
        let response = app.execute(&request, &conn).unwrap_or_else(|_| {
            let header_list = Header::get_header_list_with_config(&request, &ServerConfig::current());
            Response::get_response(
                STATUS_CODE_REASON_PHRASE.n500_internal_server_error,
                Some(header_list),
                None,
            )
        });
        (response, request)
    }

    /// Create a state-aware application. Routes registered on the returned
    /// [`AppWithState<S>`] are tried first; unmatched requests fall through to
    /// the built-in controller chain (static files, health probes, etc.).
    ///
    /// The state is stored as `Arc<S>` and shared across all handlers.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// # extern crate rws_server as rust_web_server;
    /// use rust_web_server::app::App;
    /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
    /// use rust_web_server::core::New;
    ///
    /// struct Db { url: String }
    ///
    /// let app = App::with_state(Db { url: "postgres://...".to_string() })
    ///     .get("/ping", |_req, _params, _conn, db| {
    ///         let mut r = Response::new();
    ///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
    ///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
    ///         r
    ///     });
    /// ```
    pub fn with_state<S: Send + Sync + 'static>(state: S) -> AppWithState<S> {
        AppWithState::new(state)
    }

    /// Wrap this application in a middleware layer.
    ///
    /// Returns a [`WithMiddleware<App>`] that runs `layer` before every
    /// request. Chain `.wrap()` calls to stack multiple layers:
    ///
    /// ```rust,no_run
    /// # extern crate rws_server as rust_web_server;
    /// use rust_web_server::app::App;
    /// use rust_web_server::middleware::RateLimitLayer;
    /// use rust_web_server::core::New;
    ///
    /// let app = App::new().wrap(RateLimitLayer);
    /// ```
    pub fn wrap<M: Middleware + 'static>(self, layer: M) -> WithMiddleware<App> {
        WithMiddleware::new(self).wrap(layer)
    }

    /// Attach an MCP server to this application. Tools, resources, and
    /// prompts are registered on the returned [`McpServer`]; requests that
    /// do not match the MCP endpoint are forwarded to `self` (static files,
    /// health probes, any custom routes registered before this call).
    ///
    /// ```rust,no_run
    /// # extern crate rws_server as rust_web_server;
    /// use rust_web_server::app::App;
    /// use rust_web_server::mcp::{McpContent, extract_arg};
    /// use rust_web_server::core::New;
    ///
    /// // Pure MCP — unmatched paths handled by built-in App
    /// let app = App::new()
    ///     .mcp("my-server", "1.0")
    ///     .tool(
    ///         "echo",
    ///         "Echo text back",
    ///         r#"{"type":"object","properties":{"text":{"type":"string"}}}"#,
    ///         |args| Ok(McpContent::text(extract_arg(args, "text").unwrap_or_default())),
    ///     );
    /// ```
    ///
    /// To combine with custom HTTP routes, start from [`App::with_state`]:
    ///
    /// ```rust,no_run
    /// # extern crate rws_server as rust_web_server;
    /// use rust_web_server::app::App;
    /// use rust_web_server::mcp::McpContent;
    /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
    /// use rust_web_server::core::New;
    ///
    /// let app = App::with_state(())
    ///     .get("/api/ping", |_, _, _, _| {
    ///         let mut r = Response::new();
    ///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
    ///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
    ///         r
    ///     })
    ///     .mcp("my-server", "1.0")
    ///     .tool("ping", "Ping the server", "{}", |_| Ok(McpContent::text("pong")));
    /// ```
    pub fn mcp(self, name: impl Into<String>, version: impl Into<String>) -> McpServer {
        McpServer::new(name, version).wrap(self)
    }

    /// Create an async state-aware application (requires the `http2` feature).
    ///
    /// Handlers are `async fn` closures that can `await` database queries,
    /// HTTP clients, or any other async I/O. Unmatched routes fall through to
    /// the built-in controller chain.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// # extern crate rws_server as rust_web_server;
    /// use std::sync::Arc;
    /// use rust_web_server::app::App;
    /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
    /// use rust_web_server::core::New;
    ///
    /// struct Db { url: String }
    ///
    /// let app = App::with_async_state(Db { url: "postgres://...".to_string() })
    ///     .get("/ping", |_req, _params, _conn, state| async move {
    ///         // state: Arc<Db>
    ///         let mut r = Response::new();
    ///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
    ///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
    ///         r
    ///     });
    /// ```
    #[cfg(feature = "http2")]
    pub fn with_async_state<S: Send + Sync + 'static>(state: S) -> crate::async_state::AsyncAppWithState<S> {
        crate::async_state::AsyncAppWithState::new(state)
    }
}