churust-cors 0.1.1

CORS plugin (preflight + headers) for the Churust web framework.
Documentation

Churust gives you Ktor's developer experience in Rust: an application engine, a routing DSL, an install(plugin) system, and a phased interceptor pipeline — built on a battle-tested async stack (tokio + hyper + rustls). Churust owns the ergonomic layer; it does not reinvent HTTP parsing or TLS.

use churust::prelude::*;

#[churust::main]
async fn main() -> std::io::Result<()> {
    Churust::server()
        .port(8080)
        .routing(|r| {
            r.get("/", |_call: Call| async { "Hello from Churust 🌀" });
            r.get("/users/{id}", |Path(id): Path<u64>| async move {
                format!("user #{id}")
            });
        })
        .start()
        .await
}

Install

[dependencies]
churust = "0.1"
tokio = { version = "1", features = ["full"] }

Plugins and transports are opt-in features on the umbrella crate — depend on churust and enable what you need rather than pulling in the plugin crates directly:

churust = { version = "0.1", features = ["full", "ws", "fs", "tls"] }
Feature Pulls in Gives you
json churust-json Json<T> extractor/responder, content negotiation
logging churust-logging CallLogging via tracing
cors churust-cors preflight + CORS headers
auth churust-auth Bearer/Basic/JWT, Principal<P>
full all four above the whole plugin set
ws churust-core/ws WebSocket upgrade + WebSocket/Message
fs churust-core/fs StaticFiles directory handler
tls churust-core/tls rustls-backed HTTPS

Default features are empty, so a plain churust = "0.1" compiles the core engine and nothing else.

Every Churust crate is released in lockstep on one version number, so churust-core, churust-json, and the rest always match the umbrella version.

Features

  • Hybrid handlers — write call-style (|call: Call|) or with typed extractors (Path<T>, Query<T>, State<T>, Json<T>, BearerToken, Principal<P>) — mix freely in one handler.
  • install(plugin) — Ktor-style plugins composed over a named pipeline (Setup → Monitoring → Plugins → Call → Fallback) for deterministic order.
  • Built-in plugins — JSON content negotiation, request logging, CORS, and authentication (Bearer / Basic / JWT).
  • Typed app state / DI.state(T) then extract State<T>.
  • Layered config — defaults < churust.toml < env (CHURUST_*) < code DSL.
  • Secure by default — body-size limits, request timeouts, panic isolation (a panicking handler returns 500, never crashes the server), no version banner, opt-in rustls TLS.
  • Fast, in-process testsTestClient drives the full pipeline without binding a socket.

Workspace layout

Crate Docs What it is
churust docs.rs Umbrella crate + prelude. Depend on this. Plugins behind features.
churust-core docs.rs Engine, routing, pipeline, Call, extractors, config, state, TLS, WebSockets, static files, test harness.
churust-macros docs.rs #[churust::main].
churust-json docs.rs Json<T> extractor/responder + ContentNegotiation plugin (feature json).
churust-logging docs.rs CallLogging plugin via tracing (feature logging).
churust-cors docs.rs Cors plugin — preflight + headers (feature cors).
churust-auth docs.rs Auth (Bearer/Basic/JWT) + Principal<P> (feature auth).

Runnable examples:

Example Run it Shows
examples/hello cargo run -p hello Minimal server, path params.
examples/api cargo run -p api JSON CRUD with all four plugins + auth-gated routes.
examples/chat cargo run -p chat WebSocket echo endpoint and a broadcast room.
examples/static cargo run -p static-example StaticFiles plus a streamed response body.

A fuller example

use churust::prelude::*;
use serde::{Deserialize, Serialize};
use std::sync::Mutex;

#[derive(Clone, Serialize, Deserialize)]
struct Note { id: u64, text: String }
#[derive(Deserialize)]
struct NewNote { text: String }
#[derive(Clone)]
struct Admin { name: String }
struct Store { notes: Mutex<Vec<Note>> }

#[churust::main]
async fn main() -> std::io::Result<()> {
    Churust::server()
        .state(Store { notes: Mutex::new(Vec::new()) })
        .install(CallLogging::new())
        .install(ContentNegotiation::new())  // renders errors as JSON
        .install(Cors::permissive())
        .install(Auth::bearer(|token: String| async move {
            (token == "admin-token").then(|| Admin { name: "admin".into() })
        }))
        .routing(|r| {
            r.get("/notes", |s: State<Store>| async move {
                Json(s.notes.lock().unwrap().clone())
            });
            // Asking for `Principal<Admin>` enforces auth (401 otherwise).
            r.post("/notes", |Principal(_a): Principal<Admin>,
                              s: State<Store>,
                              Json(input): Json<NewNote>| async move {
                let mut notes = s.notes.lock().unwrap();
                let note = Note { id: notes.len() as u64 + 1, text: input.text };
                notes.push(note.clone());
                (StatusCode::CREATED, Json(note))
            });
        })
        .start()
        .await
}
$ curl localhost:8080/notes
[]
$ curl -X POST localhost:8080/notes -d '{"text":"hi"}'
{"error":"authentication required","status":401}
$ curl -X POST localhost:8080/notes -H 'authorization: Bearer admin-token' \
       -H 'content-type: application/json' -d '{"text":"hi"}'
{"id":1,"text":"hi"}

WebSockets (feature ws)

Opt in with features = ["ws"]. A handler takes the WebSocketUpgrade extractor and calls on_upgrade to switch the request into a bidirectional socket (a plain GET to the route is rejected with 426 Upgrade Required):

use churust::prelude::*;
use churust::ws::{Message, WebSocketUpgrade};

r.get("/echo", |ws: WebSocketUpgrade| async move {
    ws.on_upgrade(|mut sock| async move {
        while let Some(Ok(msg)) = sock.recv().await {
            if matches!(msg, Message::Close) || sock.send(msg).await.is_err() {
                break;
            }
        }
    })
});
churust = { version = "0.1", features = ["ws"] }

See examples/chat for an echo endpoint plus a broadcast room.

Static files & streaming (feature fs)

Response bodies are a Body — either buffered bytes or a lazy stream — so large or dynamic payloads never have to be fully materialized in memory. Body is always available; StaticFiles is behind the opt-in fs feature.

use churust::prelude::*;        // brings StaticFiles into scope under `fs`
use churust::Body;

r.get(
    "/{path...}",
    StaticFiles::dir("./public").index("index.html").handler(),
);
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))
});

StaticFiles detects the Content-Type from the file extension, serves an optional index file for directories, rejects path traversal (.., absolute paths, symlink escapes) with 404, and streams the file in chunks. See examples/static.

churust = { version = "0.1", features = ["fs"] }

Core concepts

Handlers & extractors

A handler is an async closure/fn returning anything that implements IntoResponse. Arguments are extractors:

  • FromCallParts (borrow the request) — any position: Path<T>, Query<T>, State<T>, BearerToken, Principal<P>.
  • FromCall (consume the body) — last argument only: Json<T>, Call.

Call itself is the call-style base case (|call: Call| ...).

Pipeline & plugins

A Plugin registers Middleware into a Phase. Middleware own the Call, may mutate it, call next.run(call), and post-process the Response (the onion model). Phase order is fixed and deterministic regardless of install order.

Configuration

# churust.toml
[server]
host = "0.0.0.0"
port = 8080
max_body_bytes = 1048576
request_timeout_ms = 30000

[tls]            # requires the `tls` feature
cert = "cert.pem"
key  = "key.pem"

Churust::from_config() loads churust.toml + CHURUST_* env vars; chained DSL setters override. Example: CHURUST_SERVER_PORT=9090.

Testing

use churust::TestClient;

let app = build_app();
let client = TestClient::new(app);
let res = client.get("/users/1").send().await;
assert_eq!(res.status(), StatusCode::OK);

Development

Requires Rust 1.96+ (the MSRV, and what CI pins).

git clone https://github.com/davthecoder/Churust.git
cd Churust
cargo test --workspace

Warnings are errors in CI. Before opening a PR, run the full gate — the exact command list is in CONTRIBUTING.md, and it covers fmt, the clippy feature matrix, the test feature matrix, the examples, and a docs build.

Status

Published on crates.io — the badge above carries the current version. Pre-1.0, so the API is settling rather than settled: expect breaking changes in minor releases until 1.0.

Everything documented above works today:

  • Core — routing, hybrid extractors, the four plugins, named phases, layered config, typed state, timeouts, TLS, #[churust::main]
  • WebSockets — opt-in ws feature
  • Streaming bodies — the always-on Body type
  • Static filesStaticFiles, opt-in fs feature

Deferred on purpose (YAGNI): sessions, response compression, HTTP/3, route-scoped middleware sugar. Want one of them? Make the case in Discussions.

Design specs live in docs/design/ and implementation plans in docs/plans/. Those documents label work as v1 / v2.0 / v2.1 — those are internal build milestones, not published versions.

Contributing

Contributions are welcome — read CONTRIBUTING.md first. The short version: Churust keeps a narrow scope, tests come first, and anything optional is feature-gated so default builds never change.

Ask a question Discussions → Q&A
Propose a feature Discussions → Ideas
Report a bug Issues
Report a vulnerability SECURITY.md — privately, never a public issue
Get help SUPPORT.md
Cut a release (maintainers) RELEASING.md

Everyone taking part is held to the Code of Conduct.

Sponsor

Churust is built and maintained in spare time. If it saves you some, you can support the work through GitHub Sponsors.

Sponsorship funds maintenance — issue triage, security response, keeping up with tokio/hyper/rustls releases — not feature bounties. Features are decided on merit in Discussions, and that stays true regardless of who is sponsoring.

License

MIT — see LICENSE.