Skip to main content

Crate astor

Crate astor 

Source
Expand description

§astor

A minimal HTTP framework for Rust services behind a reverse proxy.

Two dependencies — matchit for routing, tokio for async I/O. No hyper. No http crate. No middleware stack you didn’t ask for.

§The contract

nginx handles TLS, rate limiting, slow clients, and body-size limits. astor does not — by design. The proxy does proxy things. The framework does framework things. Every feature astor skips is one nginx already ships, tested at scale, at no cost to you.

nginx / ingress-nginx handlesastor’s take
Body-size limitsclient_max_body_size — done.
Header-size limitslarge_client_header_buffers — done.
Rate limitinglimit_req_zone / ingress annotations — done.
Slow-client protectionnginx timeouts and buffers — done.
TLS terminationnginx SSL / k8s ingress — obviously.
HTTP/2 + HTTP/3 to clientsnginx negotiates; astor speaks HTTP/1.1 upstream.

What astor covers — the only part that changes between applications:

  • RoutingRouter + matchit, O(path-length) lookup
  • Async I/O — raw tokio, no hyper
  • Graceful shutdown — SIGTERM / Ctrl-C, drains in-flight requests

§Quick start

use astor::{Method, Request, Response, Router, Server, Status};

#[tokio::main]
async fn main() {
    let app = Router::new()
        .on(Method::Delete, "/users/{id}", delete_user, ())
        .on(Method::Get,    "/users/{id}", get_user,    ())
        .on(Method::Post,   "/users",      create_user, ());

    Server::bind("0.0.0.0:3000").serve(app).await.unwrap();
}

// req.param("id") → Option<&str>
async fn get_user(req: Request) -> Response {
    let id = req.param("id").unwrap_or("unknown");
    // astor sends bytes — build them however you like:
    //   serde_json::to_vec(&user).unwrap()
    //   format!(r#"{{"id":"{id}"}}"#).into_bytes()
    Response::json(bytes)
}

// req.body() → &[u8] — parse with serde_json, simd-json, or anything
async fn create_user(req: Request) -> Response {
    if req.body().is_empty() {
        return Response::status(Status::BadRequest);
    }
    Response::builder()
        .status(Status::Created)
        .header("location", "/users/99")
        .json(bytes)
}

// Return Status directly — astor wraps it into a response
async fn delete_user(_req: Request) -> Status { Status::NoContent }

§Status codes are a type, not a number

Every IANA-registered status code is a named Status variant. You cannot pass a raw integer where a status code goes — the compiler stops you. There are no magic numbers, no typos that silently send the wrong status, no response(2040, bytes) when you meant 204.

use astor::{Response, Status};

// Named. The compiler knows these are correct.
Response::status(Status::NoContent);   // 204
Response::status(Status::NotFound);    // 404

// The builder is the same discipline — explicit at every step.
Response::builder()
    .status(Status::Created)
    .header("location", "/users/42")
    .json(bytes);

§nginx

astor assumes nginx (or any reverse proxy) has already handled the items below. It does not re-implement any of them.

Required nginx settingWhat breaks without it
proxy_buffering onastor only reads Content-Length-framed bodies — chunked bodies are silently dropped
proxy_http_version 1.1 + proxy_set_header Connection ""keep-alive pool collapses to one request per TCP connection
client_max_body_sizeclients can stream unlimited body bytes
client_header_buffer_size / large_client_header_buffersoversized headers are not rejected before reaching astor
client_body_timeout / client_header_timeoutslow clients are not dropped; astor has no timeout logic
method whitelistnginx forwards any method string — ANYTHING /path HTTP/1.1 reaches your handlers

Minimal example (two required lines shown — not a full config):

# body size — can be set per location block for per-route limits
client_max_body_size 10m;

# Example method whitelist — adjust to your service.
# Case-sensitive (~, not ~*): RFC 9110 requires uppercase; astor does not normalise.
if ($request_method !~ ^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)$) {
    return 405;
}

Full config, Kubernetes ingress, and all required settings: docs/nginx.md

§Key types

TypePurpose
RouterRegister routes — Router::new().on(method, path, handler, extra_mw)
ServerBind a port and serve — Server::bind(addr).serve(router)
RequestIncoming request — method, path, headers, body, params
ResponseOutgoing response — shortcuts + typed builder
StatusEvery IANA status code as a named variant
MethodEvery HTTP method — RFC 9110 + WebDAV + PURGE
ContentTypeCommon content-type values for Response::builder
IntoResponseImplement on your own types to return them from handlers

Re-exports§

pub use middleware::Middleware;
pub use middleware::Next;

Modules§

middleware
Middleware layer — intercept and short-circuit requests.

Structs§

Error
The error type returned by astor’s fallible operations.
Request
An incoming HTTP request, parsed from the raw TCP stream.
Response
An outgoing HTTP response.
Router
The application router.
Server
The HTTP server.

Enums§

ContentType
Common content-type values for use with [ResponseBuilder::bytes].
Method
A known HTTP method.
Status
All IANA-registered HTTP status codes.

Traits§

Handler
Implemented for every valid route handler.
IntoResponse
Conversion into an HTTP Response.