rust-web-server 17.105.0

A dependency-minimal Rust web platform: HTTP/1.1, HTTP/2, and HTTP/3 server, reverse proxy, and application framework with routing, middleware (auth, rate limiting, tracing), an MCP server, an async ORM, background jobs, object storage, and a mailer. Runs as a zero-code config-driven proxy or as a library crate. No third-party HTTP dependencies.
Documentation
# Gaps for Using rws with React

This document tracks what's missing for `rws` to be a smooth backend/host for a React frontend — either serving a built React app's static output, or acting as the JSON API a React app (dev server or SPA in production) talks to.

---

## What already works

### CORS ✅ Done

Full config surface (`RWS_CONFIG_CORS_ALLOW_ALL`, `_ALLOW_ORIGINS`, `_ALLOW_CREDENTIALS`, `_ALLOW_HEADERS`, `_ALLOW_METHODS` in `src/entry_point/mod.rs`) lets a React dev server (Vite/webpack on `localhost:5173`/`3000`) call an rws API running on another port, including credentialed requests and preflight (`OPTIONS`) handling (`src/cors/mod.rs`).

### JSON APIs ✅ Done

`serde`/`serde_json` are optional deps (`Cargo.toml:31`) gated behind the `serde` feature. `BodyText`/`Body`/`Query` extractors (`src/extract/mod.rs`) plus `serde_json::from_str`/`to_string` cover typical fetch/axios request-response shapes.

### Static file serving ✅ Done

`StaticResourceController` (`src/app/controller/static_resource/mod.rs`) serves a built `dist/`/`build/` folder with ETags, `Last-Modified`, range requests, gzip, and directory listing when no `index.html` is present.

### Auth for SPAs ✅ Done

`sso` (OIDC client), `sso-server` (rws as its own OAuth 2.0 issuer — explicitly documented for single-page apps, see `DEVELOPER.md:4511`), `JwtLayer`, `BasicAuthLayer`, cookie/session store (`src/session/`, `src/cookie/`).

### Real-time ✅ Done

WebSocket (`src/ws_proxy/`, built-in WS handling) and SSE (`McpServer`'s `GET /mcp` SSE stream is one example; SSE is also usable standalone) for live UI updates.

### File uploads ✅ Done

`src/body/multipart_form_data` handles `multipart/form-data`, covering React file-upload UI.

---

## Gaps

### ✅ No SPA fallback for the static file server — Done

`StaticResourceController::is_matching` (`src/app/controller/static_resource/mod.rs:20-80`) only matched an existing file, an existing directory, or `path.html` — it never fell back to `index.html` for an arbitrary unmatched path. A React Router (`BrowserRouter`) deep link like `GET /dashboard/settings` had no file on disk and 404'd instead of being served `index.html` so client-side routing could take over.

The only documented SPA-fallback pattern in this codebase (`docs/src/content/docs/features/rewrite.md:126`, `.response_status(200, "OK")`) rewrites a *reverse-proxied upstream's* 404 response — it doesn't apply to the local static file server, which never proxies anywhere.

**Closed exactly as suggested:** `RWS_CONFIG_SPA_FALLBACK` (unset by default — disabled) names a file (e.g. `index.html`) that `StaticResourceController` now serves for a `GET`/`HEAD` request matching no real file/directory/`path.html`, instead of falling through to `404`. Read fresh on every request via new `entry_point::get_spa_fallback()`/`get_spa_fallback_exclude_prefixes()` helpers (mirroring `get_max_body_size`'s pattern), so it takes effect via `rws.config.toml` + `SIGHUP` with no restart, and needs no CLI-flag registration (consistent with how `RWS_CONFIG_MAX_BODY_SIZE_IN_BYTES`/`RWS_CONFIG_CSP` are also env-var/config-file-only, not part of the original hardcoded CLI-arg list).

**Scoped to avoid swallowing real API 404s** via two independent, automatic checks: (1) an extension heuristic — only a request whose *last path segment* has no `.` gets the fallback (`/dashboard/settings` qualifies; `/assets/logo.png`, a missed static asset, still 404s) — the same heuristic webpack-dev-server's `historyApiFallback`/`sirv`/`vite preview` use; (2) new `RWS_CONFIG_SPA_FALLBACK_EXCLUDE_PREFIXES` (comma-separated path prefixes, e.g. `/api,/healthz`) that never receive the fallback even when configured. The fallback file must actually exist on disk — a typo'd config value is a silent no-op, not a broken response. A file/directory that does exist is always served as itself, unchanged.

**A real, hard-to-reproduce test-isolation bug found and fixed along the way:** `RWS_CONFIG_SPA_FALLBACK` is the first `RWS_CONFIG_*` var `StaticResourceController::is_matching` depends on, and roughly a dozen pre-existing tests across `static_resource`, `async_state`, `state`, `test_client`, and `server` assert `404`/`!is_success()` for an unmatched no-extension path *without* holding `test_env::lock()` — because until now, nothing in that code path ever wrote a `RWS_CONFIG_*` var, so no lock was needed. Verified this was a real, reproducible race (not theoretical) via `cargo test --lib -- --test-threads=16` run ~15–20 times, which failed intermittently until every affected test took the lock. One unrelated pre-existing flake (`server::tests::no_expect_header_never_sends_100_continue`, racing on `RWS_CONFIG_MAX_BODY_SIZE_IN_BYTES` against a neighboring test) surfaced during the same stress testing and was fixed as a drive-by.

8 new tests covering: disabled-by-default, serving the configured file for an unmatched route, a real file still winning over the fallback, the extension heuristic rejecting an asset-shaped path, exclude-prefix scoping, a missing configured file being a no-op, and the root path (`/`) staying excluded regardless (`IndexController`'s territory). Verified end-to-end against a real running server for all four scenarios (deep link → 200 shell, missing asset → 404, real asset → 200, root → 200) plus exclude-prefix behavior. Verified across default, `http1`-only — 1544 tests green (up from 1536), zero regressions, and stable under repeated high-thread-count stress runs.

### No typed `Json<T>` extractor/responder

JSON support exists via the `serde` feature, but there's no `Json<T>` wrapper analogous to Axum's — extracting a typed request body or returning a typed JSON response means hand-writing `serde_json::from_str::<T>(&BodyText::from_request(request)?.0)` and building the `Content-Type: application/json` response manually each time.

**Remaining:** a `Json<T>` extractor (`FromRequest` impl deserializing the body, 400 on parse failure) and a `Json<T>` responder (serializes to a `200 application/json` `Response`), mirroring the pattern of `Body`/`BodyText` in `src/extract/mod.rs`.

### No dev-mode asset proxying / HMR

rws is a production HTTP server, not a bundler — there's no built-in "proxy `/api/*` to rws, everything else to the Vite/webpack dev server" story for local development. `ReverseProxy`/`RewriteLayer` can be wired manually to achieve this, but it isn't documented as a React-specific recipe.

**Remaining:** a `docs/` recipe showing `RewriteLayer` + `ReverseProxy` (or just running Vite separately and pointing its dev-server proxy config at rws) as the recommended local-dev setup.

### No React-specific CSRF recipe

The general `csrf` module exists and works, but there's no documented double-submit-cookie or header-token pattern specifically for a fetch/axios-based SPA (as opposed to a traditional server-rendered form).

**Remaining:** a `docs/` example showing the CSRF token round-trip (issue on `GET`, read via a non-`HttpOnly` cookie, echo back in a request header) for a React frontend.

---

## Summary table

| # | Gap | Area | Effort |
|---|---|---|---|
| 1 | SPA fallback (`index.html`) for unmatched static routes | Static file server | ✅ Done |
| 2 | Typed `Json<T>` extractor/responder | Extractors | Small |
| 3 | Dev-mode proxy recipe (rws + Vite/webpack dev server) | Docs | Trivial |
| 4 | CSRF recipe for fetch/axios SPAs | Docs | Trivial |

## Suggested implementation order

1. **SPA fallback** — ✅ done. Was the highest-friction gap in practice: any React Router app with deep links was broken under `cargo run`/`rws` without a hand-rolled workaround.
2. **`Json<T>` extractor/responder** — small, mechanical, and removes boilerplate from every JSON handler a React frontend talks to.
3. **Dev-mode proxy recipe** and **CSRF recipe** — documentation-only, no code changes required, but close the remaining "how do I actually wire this up" gap for a React consumer.