rust-web-server 17.14.0

Static file web server and HTTP toolkit written in Rust. Supports HTTP/3, HTTP/2, and HTTP/1.1. HTTP/3 and HTTP/2 require a TLS certificate; without one the server falls back to plain HTTP/1.1 automatically.
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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
[Read Me](README.md) > Framework Roadmap

# Framework Roadmap

What is needed to evolve `rust-web-server` from an HTTP toolkit into a framework suitable for serious application development. Items are ordered by impact — the first three unlock everything else.

---

## Blockers — cannot build a real application without these

### ✅ 1. Shared application state — _Done (v17.9.0)_

`AppWithState<S>` in `src/state/mod.rs` wraps any `S: Send + Sync` behind an `Arc` and exposes route registration with state access:

```rust
let app = AppWithState::new(AppState { db: pool, config })
    .get("/users/:id", |_req, params, _conn, state| {
        let user = state.db.find(params.get("id").unwrap()).unwrap();
        // ... build response
    });
```

Handlers receive `(&Request, &PathParams, &ConnectionInfo, &S)`. Unmatched routes fall through to the built-in [`App`] controller chain. The `Arc<S>` is cloned once per route registration, not per request.

---

### 2. Dynamic routing with path parameters

Routes are hardcoded if/else chains in `App::execute`. There is no way to define `/users/:id` and receive `id` as a value — every handler must manually parse the URI string. A REST API with 20 endpoints means 20 manual string operations, and no route is declarative or inspectable.

**Target API:**
```rust
app.get("/users/:id", UserController::show);
app.post("/users",    UserController::create);
app.delete("/users/:id", UserController::destroy);
```

The router extracts named segments (`:id`) and wildcard segments (`*path`) and makes them available in the handler as typed values.

>**Done (v17.9.0):** `AppWithState<S>` (see Item 1) integrates `Router`-style `:param` / `*wildcard` matching with shared state. Standalone `Router` (v17.6.0) remains available for stateless dispatch inside custom `Application` implementations.

---

### ✅ 3. Middleware pipeline — _Done (v17.9.0)_

`Middleware` trait and `WithMiddleware<A>` in `src/middleware/mod.rs`. Wraps any `Application`:

```rust
let app = WithMiddleware::new(App::new())
    .wrap(AuthMiddleware::new(secret))
    .wrap(RateLimitMiddleware::new(100))
    .wrap(RequestLogger);
```

`Middleware::handle` receives `next: &dyn Application` — call `next.execute` to continue the chain or return early to short-circuit. Layers run in registration order on the request path and in reverse on the response path. Composes cleanly with `AppWithState`:

```rust
let app = WithMiddleware::new(AppWithState::new(state).get(...))
    .wrap(LoggingMiddleware);
```

---

### ✅ 4. HTTP/1.1 keep-alive (persistent connections) — _Done (v17.4.0)_

Every request requires a new TCP handshake. A browser loading a page with 10 assets makes 10 TCP connections. `Server::process` reads one request then closes. The fix is to loop over requests on the same stream until `Connection: close` is received or the read times out.

---

## Major gaps — severely limits real-world use

### ✅ 5. Async handlers — _Done (v17.11.0)_

`AsyncAppWithState<S>` in `src/async_state/mod.rs` (requires `http2` feature) gives handlers an `async fn` signature so they can `await` database queries, HTTP clients, or any other async I/O:

```rust
let app = AsyncAppWithState::new(db_pool)
    .get("/users/:id", |_req, params, _conn, state| async move {
        let id = params.get("id").unwrap();
        let user = state.find_user(id).await?;
        // ... build response
    });
```

Handler signature: `Fn(Request, PathParams, ConnectionInfo, Arc<S>) -> Fut` where `Fut: Future<Output = Response> + Send + 'static`. Handlers receive owned values so the future is `'static` and can be moved freely. Full `:param` / `*wildcard` path matching is included. Unmatched routes fall through to the built-in `App` controller chain.

Entry point: `App::with_async_state(state)` (requires the `http2` Cargo feature).

---

### ✅ 6. Typed request extractors — _Done (v17.7.0)_

`FromRequest` trait in `src/extract/mod.rs`. Built-in extractors:
- `Body` — raw bytes (never fails)
- `BodyText` — UTF-8 string (returns 400 on invalid UTF-8)
- `Query` — parsed query parameters as `HashMap<String, String>`
- `RequestHeaders` — all request headers with case-insensitive `get`

Implement `FromRequest` on your own type for custom extraction logic.

---

### ✅ 7. Duplicate dispatch logic — _Done (v17.6.0)_

`App::execute` and `App::handle_request` were nearly identical if/else chains over the same controllers. Adding one route required editing both. Fixed: `App::handle_request` now delegates to `App::execute` with a synthetic `ConnectionInfo`, eliminating the duplicate dispatch code.

---

### ✅ 8. Streaming responses / chunked transfer encoding — _Done (v17.4.0)_

Every response body is fully assembled in memory before the first byte is sent. A 500 MB file allocates 500 MB of RAM and holds it for the entire write. Both `Transfer-Encoding: chunked` (HTTP/1.1) and the native stream framing in HTTP/2 and HTTP/3 need to be wired to an iterator or async stream that the controller produces incrementally.

---

### ✅ 9. Typed error handling — _Done (v17.6.0)_

`Application::execute` returns `Result<Response, String>`. Production code needs typed errors that carry their own HTTP status code, so a handler can return `Err(AppError::NotFound)` and the framework maps it to a 404 without the handler building the response manually.

`IntoResponse` trait and `AppError` enum are in `src/error/mod.rs`. `AppError` covers 400, 401, 403, 404, 409, 422, and 500. Implement `IntoResponse` on your own error type for custom mappings.

---

## Secondary gaps — painful in practice

### ✅ 10. Cookies — _Done (v17.4.0)_

`CookieJar` parses the `Cookie` request header. `SetCookie` builds `Set-Cookie` response values with all RFC 6265 attributes.

---

### ✅ 11. Response compression — _Done (v17.4.0)_

Automatic gzip compression for text responses when the client sends `Accept-Encoding: gzip`.

---

### ✅ 12. `ConnectionInfo` peer address type — _Done (v17.8.0)_

`ConnectionInfo::peer_addr() -> Option<SocketAddr>` and `Address::to_socket_addr() -> Option<SocketAddr>` helpers added as non-breaking additions. The raw `ip: String` / `port: i32` fields are preserved for backward compatibility; the helpers parse them on demand.

---

### ✅ 13. Graceful shutdown — _Done (v17.7.0)_

`Server::run` (HTTP/1.1 thread pool path, `http1` feature) now installs a Ctrl+C/SIGTERM handler via the `ctrlc` crate. On signal: the accept loop exits, `SERVER_READY` is cleared (causing `/readyz` to return 503), and `ThreadPool::join()` drains all in-flight connections before returning. The async paths (`run_tls`, `run_quic`) have handled graceful shutdown since v17.5.0.

---

### ✅ 14. No test client — _Done (v17.6.0)_

`TestClient<A>` in `src/test_client/mod.rs` dispatches requests in-process through any `Application` without opening a TCP socket.

```rust
let client = TestClient::new(App::new());
let res = client.get("/healthz").send();
assert_eq!(200, res.status());
```

---

### ✅ 15. WebSocket support — _Done (v17.8.0)_

`src/websocket/mod.rs` provides RFC 6455-compliant WebSocket protocol primitives:
- `WebSocket::is_upgrade_request(&request)` — detects Upgrade/Connection/Key headers
- `WebSocket::handshake_response(&request)` — builds the `101 Switching Protocols` response (SHA-1 accept key, base64 encoded)
- `WebSocket::read_frame(stream)` — reads one frame, handles client-to-server masking
- `WebSocket::write_frame(stream, frame)` — sends a server-to-client unmasked frame
- `Frame` enum: `Text`, `Binary`, `Ping`, `Pong`, `Close`, `Continuation`
- Convenience methods: `send_text`, `send_close`, `send_pong`

Real-time features (chat, live updates, collaborative editing) are now possible. Because WebSocket requires raw stream access after the 101 response, the handler must drive its own accept loop rather than returning from a `Controller::process` call.

---

### ✅ 16. HTTP → HTTPS redirect — _Done (v17.4.0)_

`RWS_CONFIG_HTTP_REDIRECT_PORT` binds a plain-HTTP listener that issues `301 Moved Permanently` to the HTTPS equivalent URL.

---

## Next — high-impact additions

### ✅ 17. Server-Sent Events (SSE) — _Done (v17.12.0)_

`Sse` builder and `SseEvent` in `src/sse/mod.rs` produce a correctly formatted `text/event-stream` response body from a sequence of events. Headers set automatically: `Content-Type: text/event-stream`, `Cache-Control: no-cache`, `X-Accel-Buffering: no`.

```rust
use rust_web_server::sse::{Sse, SseEvent};

let response = Sse::new()
    .event("connected", "ready")
    .push(SseEvent::data(r#"{"count":1}"#).id("1").event_type("update"))
    .push(SseEvent::data(r#"{"count":2}"#).id("2").event_type("update"))
    .retry(5000)
    .comment("keep-alive")
    .into_response();
```

`SseEvent` supports `id`, `event_type`, `retry`, and multi-line `data` (produces one `data:` line per source line, which clients join with `\n`). The response body is fully buffered before sending — suitable for pre-known event sequences. For live streaming where events arrive over time, write the SSE headers and raw event lines directly to the TCP stream in a custom accept loop (same pattern as WebSocket).

---

### ✅ 18. Session management — _Done (v17.13.0)_

`SessionStore`, `Session`, and cookie helpers in `src/session/mod.rs`. Place one `SessionStore` in your application state; it is cheap to clone (all clones share the same `Arc<Mutex<…>>` backing map).

```rust
use rust_web_server::app::App;
use rust_web_server::session::{self, SessionStore};
use rust_web_server::header::Header;

struct State { sessions: SessionStore }

let app = App::with_state(State { sessions: SessionStore::new(3600) })
    .post("/login", |req, _params, _conn, state| {
        let mut sess = state.sessions.create();
        sess.set("user_id", "42");
        state.sessions.save(&sess);
        // set cookie on response …
        let cookie = session::session_cookie(&sess.id, "sid", 3600);
        // response.headers.push(Header { name: "Set-Cookie".to_string(), value: cookie });
        // …
    })
    .get("/profile", |req, _params, _conn, state| {
        let sid = session::session_id_from_request(&req, "sid")?;
        let sess = state.sessions.load(&sid)?;
        let user_id = sess.get("user_id").unwrap_or("guest");
        // …
    });
```

API summary:
- `SessionStore::new(ttl_secs)` — create a store; sessions expire after `ttl_secs`
- `store.create()``Session` — generate ID, insert empty session
- `store.create_with_id(id)``Session` — caller-supplied ID (CSPRNG)
- `store.load(id)``Option<Session>` — returns `None` if unknown or expired
- `store.save(&session)` — persist mutations back to the store
- `store.destroy(id)` — delete a session
- `store.purge_expired()` — reclaim memory (call periodically)
- `session_id_from_request(&req, cookie_name)``Option<String>`
- `session_cookie(id, name, ttl_secs)``Set-Cookie` value (`HttpOnly`, `SameSite=Lax`)
- `destroy_cookie(name)``Set-Cookie` with `Max-Age=0`

---

### ✅ 19. Serde JSON integration — _Done (v17.14.0)_

`Json<T>` extractor and responder in `src/json/extractor.rs`, gated on the `serde` Cargo feature (adds `serde` + `serde_json` deps). Enable with `features = ["serde"]` in `Cargo.toml`.

```toml
# Cargo.toml
rust-web-server = { version = "17", features = ["serde"] }
```

```rust
use serde::{Deserialize, Serialize};
use rust_web_server::json::Json;
use rust_web_server::state::AppWithState;

#[derive(Deserialize)]
struct CreateUser { name: String, age: u32 }

#[derive(Serialize)]
struct UserResponse { id: u64, name: String }

let app = AppWithState::new(())
    .post("/users", |req, _params, _conn, _state| {
        let Json(payload) = match Json::<CreateUser>::from_request(&req) {
            Ok(j)  => j,
            Err(r) => return r,  // 400 on bad JSON
        };
        Json(UserResponse { id: 1, name: payload.name }).into_response()
    });
```

- `Json::<T>::from_request(&req)``Result<Json<T>, Response>` (400 on parse error)
- `Json(value).into_response()``200 OK` with `Content-Type: application/json`
- Implements `FromRequest` so it works with the typed extractor pattern
- `Deref<Target = T>` for transparent field access

---

### 20. Built-in auth middleware

JWT verification and HTTP Basic Auth are common enough to ship as first-party middleware rather than leaving each consumer to implement them correctly (timing-safe comparison, algorithm confusion attacks, etc.).

**Target API:**
```rust
// JWT
let app = App::new()
    .wrap(JwtLayer::new(secret).algorithm(Algorithm::HS256).claim("sub"));

// Basic Auth
let app = App::new()
    .wrap(BasicAuthLayer::new(|user, pass| user == "admin" && pass == secret));
```

---

### 21. Automatic TLS (ACME / Let's Encrypt)

Obtaining and renewing TLS certificates is manual today — the operator must run `certbot`, write the paths into config, and handle renewal restarts. ACME would automate issuance and zero-downtime renewal directly inside the server process.

**Target API:**
```rust
cargo run -- --acme-domain=example.com --acme-email=admin@example.com
```

---

## Developer experience

### 22. Declarative routing macros

The current `App::execute` registration table works but requires a separate struct per route and explicit wiring. A proc-macro attribute would eliminate the boilerplate for the common case.

**Target API:**
```rust
#[route(GET, "/users/:id")]
async fn get_user(req: Request, params: PathParams, conn: ConnectionInfo, state: Arc<Db>) -> Response {
    // ...
}
```

---

### 23. `derive(FromRequest)`

Implementing `FromRequest` for a custom extractor today requires a manual `impl FromRequest for MyType` block. A derive macro would generate it from struct field types.

**Target API:**
```rust
#[derive(FromRequest)]
struct AuthPayload {
    #[from_header("Authorization")]
    token: BearerToken,
    #[from_query("locale")]
    locale: Option<String>,
}
```

---

### 24. Request validation helpers

Field-level validation (required, min/max length, regex, numeric range) is manual today. A validation layer would run checks before the handler and return structured 422 error bodies automatically.

**Target API:**
```rust
#[derive(Validate, FromRequest)]
struct CreateUser {
    #[validate(length(min = 1, max = 50))]
    name: String,
    #[validate(email)]
    email: String,
    #[validate(range(min = 0, max = 150))]
    age: u8,
}
```

---

## Security

### 25. IP allowlist / denylist

No request filtering by client IP exists. Blocking known-bad ranges or restricting admin endpoints to an internal CIDR requires a custom `Middleware` implementation today.

**Target API:**
```rust
let app = App::new()
    .wrap(IpFilter::allow(["10.0.0.0/8", "192.168.0.0/16"]))
    .wrap(IpFilter::deny(["1.2.3.4"]));
```

---

## Infrastructure

### 26. OpenTelemetry distributed tracing

There is no trace context propagation. Requests cannot be correlated across services, and there is no way to measure handler latency with span-level granularity compatible with Jaeger, Tempo, or Honeycomb.

**Target API:**
```rust
let app = App::new()
    .wrap(OtelLayer::new(tracer).propagate_b3().propagate_w3c());
```

---

### 27. Per-route metrics

`/metrics` currently exports only server-wide counters. Production services need per-route request counts and latency histograms to identify slow endpoints and set SLO alerts.

**Target outcome:** `/metrics` includes `rws_route_requests_total{method,path,status}` and `rws_route_duration_seconds{method,path}` histograms.

---

### 28. Response caching

Every request hits the handler regardless of whether the response could be served from an in-memory or shared cache. A cache middleware would short-circuit the handler for `GET` responses within their TTL.

**Target API:**
```rust
let app = App::new()
    .wrap(CacheLayer::memory(1000).ttl(60).vary_by_header("Accept"));
```

---

### 29. Hot config reload

Configuration changes (thread count, rate-limit thresholds, TLS cert rotation) require a full server restart today. A `SIGHUP` handler that re-reads `rws.config.toml` and applies non-binding changes in-place would eliminate downtime for routine tuning.

---

### 30. Reverse proxy / load balancing

There is no way to proxy requests to upstream services. A reverse-proxy handler would let `rws` sit in front of multiple backends, enabling blue-green deploys, A/B routing, and sidecar patterns without an external Nginx or Envoy.

**Target API:**
```rust
let app = App::new()
    .wrap(ReverseProxy::new(["http://backend-1:8080", "http://backend-2:8080"])
        .strategy(LoadBalancing::RoundRobin)
        .health_check("/healthz"));
```

---

### 31. MCP (Model Context Protocol) server

AI coding agents and LLM tool-callers need a standardized interface to interact with application APIs. An `McpController` would expose tools, resources, and prompts over the MCP protocol, making any `rws` application instantly reachable from Claude, Cursor, and other MCP-aware clients.

**Target API:**
```rust
let app = App::new()
    .mcp_tool("list_users", list_users_handler)
    .mcp_resource("user://{id}", get_user_resource)
    .mcp_prompt("summarize", summarize_prompt);
```

---

## Summary

| # | Item | Status |
|---|------|--------|
| 1 | Shared application state | ✅ Done (v17.9.0) |
| 2 | Dynamic routing with path parameters | ✅ Done (v17.9.0) |
| 3 | Middleware pipeline | ✅ Done (v17.9.0) |
| 4 | HTTP/1.1 keep-alive | ✅ Done (v17.4.0) |
| 5 | Async handlers | ✅ Done (v17.11.0) |
| 6 | Typed request extractors | ✅ Done (v17.7.0) |
| 7 | Duplicate dispatch logic | ✅ Done (v17.6.0) |
| 8 | Streaming responses | ✅ Done (v17.4.0) |
| 9 | Typed error handling | ✅ Done (v17.6.0) |
| 10 | Cookies | ✅ Done (v17.4.0) |
| 11 | Response compression | ✅ Done (v17.4.0) |
| 12 | `ConnectionInfo` uses `String` not `SocketAddr` | ✅ Done (v17.8.0) |
| 13 | Graceful shutdown | ✅ Done (v17.7.0) |
| 14 | No test client | ✅ Done (v17.6.0) |
| 15 | WebSocket support | ✅ Done (v17.8.0) |
| 16 | HTTP → HTTPS redirect | ✅ Done (v17.4.0) |
| 17 | Server-Sent Events (SSE) | ✅ Done (v17.12.0) |
| 18 | Session management | ✅ Done (v17.13.0) |
| 19 | Serde JSON integration | ✅ Done (v17.14.0) |
| 20 | Built-in auth middleware (JWT + Basic) | Pending |
| 21 | Automatic TLS (ACME / Let's Encrypt) | Pending |
| 22 | Declarative routing macros | Pending |
| 23 | `derive(FromRequest)` | Pending |
| 24 | Request validation helpers | Pending |
| 25 | IP allowlist / denylist | Pending |
| 26 | OpenTelemetry distributed tracing | Pending |
| 27 | Per-route metrics | Pending |
| 28 | Response caching | Pending |
| 29 | Hot config reload | Pending |
| 30 | Reverse proxy / load balancing | Pending |
| 31 | MCP server controller | Pending |