-
Notifications
You must be signed in to change notification settings - Fork 1
HTTP Server
By default the server runs an event-driven scheduler: an accept thread
hands sockets to an event loop that parks idle/partial plain-HTTP
connections on a readiness poller (Winsock select / POSIX poll), so a
herd of keep-alive / SSE / slow-loris connections consumes zero
workers. A ready connection is handed to one of a small set of event
workers, which run an incremental request parser that resumes where it
left off; a slow sender is parked again instead of holding a worker.
TLS and HTTP/2 connections run on the work-stealing thread pool, so
connection handling still scales across cores. A handler is just a
function — no framework types to implement.
A handler is any Fn(Request<Body>) -> Response<Body> + Send + Sync + 'static. Closures work out of the box:
use courierust::courierust_body::Body;
use courierust::courierust_bytes::Bytes;
use courierust::courierust_http::request::Request;
use courierust::courierust_http::response::Response;
let handler = |req: Request<Body>| -> Response<Body> {
let mut resp = Response::with_status(200.into());
resp.body = Body::Bytes(Bytes::from(format!("path: {}", req.uri.as_str())));
resp
};For anything non-trivial, use a struct (the fields are your own application state):
use courierust::courierust_body::Body;
use courierust::courierust_bytes::Bytes;
use courierust::courierust_http::request::Request;
use courierust::courierust_http::response::Response;
use std::sync::Arc;
// `Db` here is your own application type, not something from the crate.
// Handler requires Send + Sync, so Db must satisfy both.
struct App {
db: Arc<dyn Db>,
}
// Any type implementing the Handler trait works:
impl courierust::courierust_server::Handler for App {
fn handle(&self, req: Request<Body>) -> Response<Body> {
match req.uri.as_str() {
"/health" => Response::with_status(200.into()),
_ => {
let mut resp = Response::with_status(404.into());
resp.body = Body::Bytes(Bytes::from_static(b"not found"));
resp
}
}
}
}use courierust::courierust_server::{Server, ServerConfig};
use std::time::Duration;
let cfg = ServerConfig {
// Serve HTTP/2 (prior knowledge) on the same port as HTTP/1.1.
http2: true,
// Worker threads; 0 = bounded auto sizing (1-8 workers).
threads: 0,
read_timeout: Some(Duration::from_secs(120)),
max_header_list: 1 << 20,
max_body: 16 * 1024 * 1024,
};let server = Server::bind_with_config("0.0.0.0:8080", cfg)?;
server.serve(app)?; // blocks foreverlet server = Server::bind_with_config("127.0.0.1:0", cfg)?;
let addr = server.local_addr()?; // real bound port
let handle = server.serve_background(app)?;
// ... run tests / do other work ...
// Dropping the handle stops accepting; connections drain.
drop(handle);Return a Body::Channel and the server streams it with flow-control backpressure: chunks are only drained from the channel when the connection has send window available, so a slow client cannot balloon memory.
let handler = |_req: Request<Body>| -> Response<Body> {
let (tx, body) = courierust::courierust_body::channel();
std::thread::spawn(move || {
for i in 0..100 {
// tx.send blocks if the receiver is dropped; returns Result.
let _ = tx.send(Bytes::from(format!("event {i}\n")));
}
drop(tx); // closing the sender ends the stream
});
let mut resp = Response::with_status(200.into());
resp.body = body;
resp
};Send an error mid-stream with tx.fail(err) — the connection resets that stream with INTERNAL_ERROR.
-
The default is the event-driven connection path. It parks incomplete and slow plain HTTP/1.1 connections without assigning one worker per socket.
max_connectionsdefaults to 1024;0is an explicit unlimited setting.event_driven: falseis a legacy compatibility mode and must be paired with a finitemax_connectionsin exposed deployments. -
HTTP/1.1 responses without a body get an explicit
Content-Length: 0; chunked encoding is emitted when the length is unknown. -
The event scheduler is the default on every platform for plain HTTP/1.1: a partial request parks on the poller (zero workers), and connections idle for
ServerConfig::idle_timeoutare reaped.max_connectionscaps the parked population. TLS and HTTP/2 connections run on the blocking pool, bounded byhandshake_timeout/h2_idle_timeout. Settingevent_driven: falserestores the legacy one-pool-job-per-connection model. -
A synchronous handler that blocks holds its event worker for as long as it blocks — exactly like any synchronous server. Use a channel body (
Body::Channel, orcourierust_body::channel()) for streaming so the worker returns promptly: while the producer is between chunks the connection is parked, and the producer'ssendwakes the reactor (Body::Stream); a rawBody::Channelhas no producer-side wake and is polled instead. A streamed response to aConnection: closerequest is written in full before the connection closes. -
gRPC servers are a thin layer on this server — see gRPC.
The same server upgrades HTTP/1.1 connections into WebSockets: implement
Handler::websocket and answer WsUpgradeReply::Accept(service) for the
paths you own (Pass keeps the request on the HTTP path). No second port,
no second listener, and no per-connection thread: in the default event
driver an idle WebSocket holds one poller slot and zero workers.
use courierust::courierust_server::ws::WsUpgradeReply;
use std::sync::Arc;
impl Handler for App {
// ... handle() as usual ...
fn websocket(&self, req: &Request<Body>) -> WsUpgradeReply {
if req.path == "/ws" {
WsUpgradeReply::Accept(Arc::new(Echo))
} else {
WsUpgradeReply::Pass
}
}
}WsConfig covers the Origin policy, subprotocols, the frame / message /
fragment / send-queue limits, permessage-deflate, the Ping/Pong
keepalive and trusted_proxies — and both drivers run the same engine, so
the policy cannot depend on which one is active. The full tutorial (server
hook, client, proxy deployment, the rules enforced for you) is
WebSockets.