Responses & errors
Building responses
Handlers return types that implement IntoResponse:
#![allow(unused)] fn main() { // 200 text/plain-ish defaults for strings r.get("/ping", |_c: Call| async { "pong" }); // Explicit status + body r.post("/items", |_c: Call| async { (StatusCode::CREATED, "created") }); // JSON (feature `json`) r.get("/note", |_c: Call| async { Json(serde_json::json!({ "ok": true })) }); }
Streaming
Body is always available (no feature flag). Stream large or dynamic payloads
without buffering everything in memory:
#![allow(unused)] fn main() { use churust::Body; r.get("/numbers", |_c: Call| async { let chunks = futures_util::stream::iter( (1..=5).map(|i| Ok::<_, std::io::Error>(bytes::Bytes::from(format!("{i}\n")))), ); Response::stream("text/plain", Body::from_stream(chunks)) }); }
Custom error pages
Use on_error on the app builder to render any 4xx / 5xx, including routing
failures, with your own HTML or JSON.
With ContentNegotiation installed (feature json), errors can be rendered as
JSON for API clients.
IntoError
Implement IntoError on your domain error type so handlers can use ?. Churust
maps the error to an HTTP status and a safe client-facing body without exposing
internal details from Display by default.
Transport-level errors
Security headers and limits apply even when a handler never runs — for example
413 (body too large), 400, and 408 produced by the transport. See
Security defaults.
Correct HTTP semantics
- Conditional GET for static files (
ETag/Last-Modified/304) - Byte ranges (
206/416) for static files - Ambiguous framing (both
Transfer-EncodingandContent-Length) is refused so leftover bytes cannot become the next request behind a proxy - Repeated query keys on a scalar field are an error, not silent first/last wins