WebSockets, streaming, and uploads
Handle Axum WebSockets, server-sent events, multipart uploads, and streaming bodies with Rust examples for each response pattern.
WebSockets#
A WebSocket starts as HTTP, upgrades the connection, then exchanges frames in both directions. Enable Axum’s ws feature.
use axum::{
extract::ws::{Message, WebSocket, WebSocketUpgrade},
response::Response,
routing::any,
Router,
};
async fn upgrade(ws: WebSocketUpgrade) -> Response {
ws.protocols(["notes.v1"])
.on_failed_upgrade(|error| tracing::warn!(%error, "upgrade failed"))
.on_upgrade(handle_socket)
}
async fn handle_socket(mut socket: WebSocket) {
while let Some(result) = socket.recv().await {
match result {
Ok(Message::Text(text)) => {
if socket.send(Message::Text(text)).await.is_err() {
break;
}
}
Ok(Message::Ping(bytes)) => {
let _ = socket.send(Message::Pong(bytes)).await;
}
Ok(Message::Close(_)) | Err(_) => break,
_ => {}
}
}
}
let app = Router::new().route("/ws", any(upgrade));
Message variants are Text, Binary, Ping, Pong, and Close. Configure protocols and buffer/message/frame limits on WebSocketUpgrade. For concurrent reading and writing, call socket.split() from futures_util::StreamExt and own each half in a task.
Server-sent events#
SSE is a one-way HTTP stream from server to browser. It is simpler than WebSocket when the client only needs live updates from the server.
use axum::response::sse::{Event, KeepAlive, Sse};
use futures_util::stream::{self, Stream};
use std::{convert::Infallible, time::Duration};
use tokio_stream::StreamExt;
async fn events() -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
let stream = stream::repeat_with(|| {
Ok(Event::default()
.event("heartbeat")
.data("still connected"))
})
.throttle(Duration::from_secs(5));
Sse::new(stream).keep_alive(
KeepAlive::new()
.interval(Duration::from_secs(15))
.text("keep-alive"),
)
}
Event can set data, JSON data, event, id, retry, and comments. EventDataWriter safely builds multiline data. KeepAlive prevents quiet intermediaries from closing an idle stream.
Multipart uploads#
Use Multipart for browser forms that mix fields and files. Read fields in order; a field borrows the multipart stream, so finish it before asking for the next one.
use axum::{
extract::{DefaultBodyLimit, Multipart},
http::StatusCode,
routing::post,
Router,
};
async fn upload(mut multipart: Multipart) -> Result<StatusCode, AppError> {
while let Some(field) = multipart
.next_field()
.await
.map_err(|_| AppError::Validation("invalid multipart body".into()))?
{
let name = field.name().unwrap_or("").to_owned();
let file_name = field.file_name().map(str::to_owned);
let content_type = field.content_type().map(str::to_owned);
let bytes = field
.bytes()
.await
.map_err(|_| AppError::Validation("cannot read field".into()))?;
tracing::info!(%name, ?file_name, ?content_type, size = bytes.len());
}
Ok(StatusCode::NO_CONTENT)
}
let app = Router::new()
.route("/upload", post(upload))
.layer(DefaultBodyLimit::max(10 * 1024 * 1024));
A Field exposes name, file_name, content_type, headers, bytes, text, and chunked reading. Validate file type from content, not only the client-provided name or MIME header.
Bodies & streaming#
Buffer small known-size bodies; stream large or unbounded bodies. Axum’s default buffering limit is 2 MiB for extractors such as Bytes, String, Json, and Form.
use axum::{
body::{to_bytes, Body},
extract::Request,
http::StatusCode,
};
async fn inspect(request: Request) -> Result<String, StatusCode> {
let bytes = to_bytes(request.into_body(), 64 * 1024)
.await
.map_err(|_| StatusCode::PAYLOAD_TOO_LARGE)?;
Ok(format!("received {} bytes", bytes.len()))
}
use axum::body::{Body, Bytes};
use futures_util::stream;
use std::convert::Infallible;
async fn download() -> Body {
let chunks = stream::iter([
Ok::<_, Infallible>(Bytes::from_static(b"first\n")),
Ok(Bytes::from_static(b"second\n")),
]);
Body::from_stream(chunks)
}
Body::new wraps another HTTP body, Body::empty creates an empty body, Body::from_stream adapts a stream, and into_data_stream exposes data frames. DefaultBodyLimit::max(n) changes the default buffer limit; DefaultBodyLimit::disable() removes it. Add another explicit limit before accepting untrusted data.
Services & static files#
Handlers are convenient Tower services. Use service routes when you already have a Tower service, need static files, or are integrating another HTTP component.
use axum::Router;
use tower_http::services::{ServeDir, ServeFile};
let app = Router::new()
.nest_service(
"/assets",
ServeDir::new("public/assets"),
)
.fallback_service(
ServeFile::new("public/index.html"),
);
Peer connection information
use axum::extract::ConnectInfo;
use std::net::SocketAddr;
async fn who_am_i(
ConnectInfo(peer): ConnectInfo<SocketAddr>,
) -> String {
format!("connected from {peer}")
}
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
).await?;
Behind a reverse proxy, the socket peer is usually the proxy. Only trust forwarded-IP headers from a proxy you control and configure; never accept them blindly from the public internet.
route_service mounts one service at one path. nest_service mounts a service under a prefix. The *_service routing helpers match methods and adapt service response bodies. If a service can error, wrap it in HandleError or HandleErrorLayer.
Custom extensions#
Build a custom extractor when validation belongs at the HTTP boundary and will be reused. Implement FromRequestParts if no body is needed; implement FromRequest when consuming the body.
use axum::{
extract::FromRequestParts,
http::{header, request::Parts, StatusCode},
};
struct ApiKey(String);
impl<S> FromRequestParts<S> for ApiKey
where
S: Send + Sync,
{
type Rejection = (StatusCode, &'static str);
async fn from_request_parts(
parts: &mut Parts,
_state: &S,
) -> Result<Self, Self::Rejection> {
let value = parts
.headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.ok_or((StatusCode::UNAUTHORIZED, "missing bearer token"))?;
Ok(Self(value.to_owned()))
}
}
async fn private(ApiKey(key): ApiKey) -> String {
format!("accepted key with {} characters", key.len())
}
Run extractors manually
use axum::{extract::Request, Json, RequestExt};
async fn custom_pipeline(
request: Request,
) -> Result<Json<CreateNote>, AppError> {
request
.extract::<Json<CreateNote>, _>()
.await
.map_err(|_| AppError::Validation("bad JSON".into()))
}
RequestExt and RequestPartsExt provide extract and extract_with_state. With the macros feature, #[derive(FromRequest)] and #[derive(FromRequestParts)] reduce wrapper boilerplate.
Lower-level middleware
Use from_fn for application middleware. Implement Tower’s Layer and Service when publishing reusable middleware, controlling readiness/backpressure, or avoiding boxed futures. Axum expects routed services to be effectively always ready; apply load shedding or wrap backpressure-sensitive services around the whole router.
Help improve CodeRef
Allow Google Analytics to measure page visits and use of the guides? It uses cookies after you accept. Your choice won’t affect access to the site. Privacy notice