NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
crates.io · #266 most downloaded on crates.io
HTTP routing and request handling library that focuses on ergonomics and modularity
Last release 5 months ago
14 Apr 2026
Release timing varies
gaps range from 2 weeks to 5 months
Nearly every release is documented
notes for 59 of the last 60 stable releases
5 versions withdrawn
withdrawn after publishing
5 years old
94 releases · first in 2021
One column per quarter.
fixed: Correctly handle GET, HEAD, and OPTIONS requests in ContentLengthLimit. Request with these methods are now accepted if they _do not_ have a Con
GET, HEAD, and OPTIONS requests in ContentLengthLimit.
Request with these methods are now accepted if they do not have a Content-Length header, and
the request body will not be checked. If they do have a Content-Length header they'll be
rejected. This allows ContentLengthLimit to be used as middleware around several routes,
including GET routes (#989)MethodRouter::{into_make_service, into_make_service_with_connect_info} (#1010)added: Add middleware::from_extractor and deprecate extract::extractor_middleware ([#957])
added: Add AppendHeaders for appending headers to a response rather than overriding them ([#927])
Yanked, as it contained an accidental breaking change.
Yanked, as it contained an accidental breaking change.
added: Add RequestParts::extract which allows applying an extractor as a method call ([#897])
RequestParts::extract which allows applying an extractor as a method call (#897)This includes these breaking changes:
added: Document sharing state between handler and middleware (#783)
added: Extension<_> can now be used in tuples for building responses, and will set an
extension on the response (#797)
added: extract::Host for extracting the hostname of a request (#827)
added: Add IntoResponseParts trait which allows defining custom response
types for adding headers or extensions to responses (#797)
added: TypedHeader implements the new IntoResponseParts trait so they
can be returned from handlers as parts of a response (#797)
changed: Router::merge now accepts Into<Router> (#819)
breaking: sse::Event now accepts types implementing AsRef<str> instead of Into<String>
as field values.
breaking: sse::Event now panics if a setter method is called twice instead of silently
overwriting old values.
breaking: Require Output = () on WebSocketStream::on_upgrade (#644)
breaking: Make TypedHeaderRejectionReason #[non_exhaustive] (#665)
breaking: Using HeaderMap as an extractor will no longer remove the headers and thus
they'll still be accessible to other extractors, such as axum::extract::Json. Instead
HeaderMap will clone the headers. You should prefer to use TypedHeader to extract only the
headers you need (#698)
This includes these breaking changes:
RequestParts::take_headers has been removed.RequestParts::headers returns &HeaderMap.RequestParts::headers_mut returns &mut HeaderMap.HeadersAlreadyExtracted has been removed.HeadersAlreadyExtracted variant has been removed from these rejections:
RequestAlreadyExtractedRequestPartsAlreadyExtractedJsonRejectionFormRejectionContentLengthLimitRejectionWebSocketUpgradeRejection<HeaderMap as FromRequest<_>>::Rejection has been changed to std::convert::Infallible.breaking: axum::http::Extensions is no longer an extractor (ie it
doesn't implement FromRequest). The axum::extract::Extension extractor is
not impacted by this and works the same. This change makes it harder to
accidentally remove all extensions which would result in confusing errors
elsewhere (#699)
This includes these breaking changes:
RequestParts::take_extensions has been removed.RequestParts::extensions returns &Extensions.RequestParts::extensions_mut returns &mut Extensions.RequestAlreadyExtracted has been removed.<Request as FromRequest>::Rejection is now BodyAlreadyExtracted.<http::request::Parts as FromRequest>::Rejection is now Infallible.ExtensionsAlreadyExtracted has been removed.ExtensionsAlreadyExtracted removed variant has been removed from these rejections:
ExtensionRejectionPathRejectionMatchedPathRejectionWebSocketUpgradeRejectionbreaking: Redirect::found has been removed (#800)
breaking: AddExtensionLayer has been removed. Use Extension instead. It now implements
tower::Layer (#807)
breaking: AddExtension has been moved from the root module to middleware
breaking: .nest("/foo/", Router::new().route("/bar", _)) now does the right thing and
results in a route at /foo/bar instead of /foo//bar (#824)
breaking: Routes are now required to start with /. Previously routes such as :foo would
be accepted but most likely result in bugs (#823)
breaking: Headers has been removed. Arrays of tuples directly implement
IntoResponseParts so ([("x-foo", "foo")], response) now works (#797)
breaking: InvalidJsonBody has been replaced with JsonDataError to clearly signal that the
request body was syntactically valid JSON but couldn't be deserialized into the target type
breaking: Handler is no longer an #[async_trait] but instead has an
associated Future type. That allows users to build their own Handler types
without paying the cost of #[async_trait] (#879)
changed: New JsonSyntaxError variant added to JsonRejection. This is returned when the
request body contains syntactically invalid JSON
fixed: Correctly set the Content-Length header for response to HEAD
requests (#734)
fixed: Fix wrong content-length for HEAD requests to endpoints that returns chunked
responses (#755)
fixed: Fixed several routing bugs related to nested "opaque" tower services (i.e.
non-Router services) (#841 and #842)
changed: Update to tokio-tungstenite 0.17 (#791)
breaking: Redirect::{to, temporary, permanent} now accept &str instead
of Uri (#889)
breaking: Remove second type parameter from Router::into_make_service_with_connect_info
and Handler::into_make_service_with_connect_info to support MakeServices
that accept multiple targets (#892)
Use correct path for AddExtensionLayer and AddExtension::layer deprecation notes ([#812])
AddExtensionLayer and AddExtension::layer deprecation
notes (#812)changed: Deprecate AddExtensionLayer. Use Extension instead ([#805])
added: middleware::from_fn for creating middleware from async functions. This previously lived in axum-extra but has been moved to axum ([#719])
Reference [axum-macros] instead of [axum-debug]. The latter has been superseded by axum-macros and is deprecated ([#738])
fixed: Replace response code 301 with 308 for trailing slash redirects. Also deprecates Redirect::found (302) in favor of Redirect::temporary (307) or…
Routers at / (#691)nest("", service) work and mean the same as nest("/", service) (#691)301 with 308 for trailing slash redirects. Also deprecates
Redirect::found (302) in favor of Redirect::temporary (307) or Redirect::to (303).
This is to prevent clients from changing non-GET requests to GET requests (#682)Thus PathRejection now contains a variant with ExtensionsAlreadyExtracted. This is not a breaking change since PathRejection is marked as #[non_exhaus…
axum::AddExtension::layer (#607)sse::Event will no longer drop the leading space of data, event ID and name values
that have it (#600)sse::Event is more strict about what field values it supports, disallowing any SSE
events that break the specification (such as field values containing carriage returns) (#599)sse::Event (#601)Path fail with ExtensionsAlreadyExtracted if another extractor (such as
Request) has previously taken the request extensions. Thus PathRejection now contains a
variant with ExtensionsAlreadyExtracted. This is not a breaking change since PathRejection is
marked as #[non_exhaustive] (#619)PathRejection if extensions had
previously been extracted (#619)AtomicU32 internally, rather than AtomicU64, to improve portability (#616)fix: Depend on the correct version of axum-core ([#592])
axum-core (#592)added: axum::Response now exists as a shorthand for writing Response ([#590])
axum::response::Response now exists as a shorthand for writing Response<BoxBody> (#590)axum-core has a smaller API and will thus receive fewer breaking changes. FromRequest and IntoResponse are re-exported from axum in the same location…
MethodRouter that works similarly to Router:
MethodRouter::layer and
MethodRouter::route_layer.MethodRouter::mergeMethodRouter::fallbackFromRequest and
RequestParts has been removed. Use FromRequest<Body> and
RequestParts<Body> to get the previous behavior (#564)FromRequest and IntoResponse are now defined in a new called
axum-core. This crate is intended for library authors to depend on, rather
than axum itself, if possible. axum-core has a smaller API and will thus
receive fewer breaking changes. FromRequest and IntoResponse are
re-exported from axum in the same location so nothing is changed for axum
users (#564)axum::body::box_body function has
been removed. Use axum::body::boxed instead..route("/", get(_)).route("/", post(_)).routing::handler_method_router and
routing::service_method_router has been removed in favor of
routing::{get, get_service, ..., MethodRouter}.HandleErrorExt has been removed in favor of
MethodRouter::handle_error.HandleErrorLayer now requires the handler function to be
async (#534)HandleErrorLayer now supports running extractors.Handler<B, T> trait is now defined as Handler<T, B = Body>. That is the type parameters have been swapped and B defaults to
axum::body::Body (#527)Router::merge will panic if both routers have fallbacks.
Previously the left side fallback would be silently discarded (#529)Router::nest will panic if the nested router has a fallback.
Previously it would be silently discarded (#529)charset=utf-8 for text content type. (#554)Body and BodyError associated types on the
IntoResponse trait have been removed - instead, .into_response() will now
always return Response<BoxBody> (#571)PathParamsRejection has been renamed to PathRejection and its
variants renamed to FailedToDeserializePathParams and MissingPathParams. This
makes it more consistent with the rest of axum (#574)Path's rejection type now provides data about exactly which part of
the path couldn't be deserialized (#574)changed: box_body has been renamed to boxed. box_body still exists but is deprecated ([#530])
box_body has been renamed to boxed. box_body still exists
but is deprecated (#530)Implement FromRequest for [http::Parts] so it can be used an extractor ([#489])
FromRequest for http::request::Parts so it can be used an
extractor (#489)IntoResponse for http::response::Parts (#490)added: Add Router::route_layer for applying middleware that will only run on requests that match a route. This is useful for middleware that return ea
Router::route_layer for applying middleware that
will only run on requests that match a route. This is useful for middleware
that return early, such as authorization (#474)fixed: Implement Clone for IntoMakeServiceWithConnectInfo ([#471])
Clone for IntoMakeServiceWithConnectInfo (#471)This enables removing a few dependencies if your app only uses HTTP2 or doesn't use JSON. This is only a breaking change if you depend on axum with de…
fixed: All known compile time issues are resolved, including those with
boxed and those introduced by Rust 1.56 (#404)
breaking: The router's type is now always Router regardless of how many routes or
middleware are applied (#404)
This means router types are all always nameable:
fn my_routes() -> Router {
Router::new().route(
"/users",
post(|| async { "Hello, World!" }),
)
}
breaking: Added feature flags for HTTP1 and JSON. This enables removing a
few dependencies if your app only uses HTTP2 or doesn't use JSON. This is only a
breaking change if you depend on axum with default_features = false. (#286)
breaking: Route::boxed and BoxRoute have been removed as they're no longer
necessary (#404)
breaking: Nested, Or types are now private. They no longer had to be
public because Router is internally boxed (#404)
breaking: Remove routing::Layered as it didn't actually do anything and
thus wasn't necessary
breaking: Vendor AddExtensionLayer and AddExtension to reduce public
dependencies
breaking: body::BoxBody is now a type alias for
http_body::combinators::UnsyncBoxBody and thus is no longer Sync. This
is because bodies are streams and requiring streams to be Sync is
unnecessary.
added: Implement IntoResponse for http_body::combinators::UnsyncBoxBody.
added: Add Handler::into_make_service for serving a handler without a
Router.
added: Add Handler::into_make_service_with_connect_info for serving a
handler without a Router, and storing info about the incoming connection.
breaking: axum's minimum supported rust version is now 1.56
.route("/api/users/*rest", service) are now supported.Router::route call. So .route("/", get(get_handler).post(post_handler)) and
not .route("/", get(get_handler)).route("/", post(post_handler)).axum::handler
to axum::routing. So axum::handler::get now lives at axum::routing::get
(#405)axum::service
to axum::routing::service_method_routing. So axum::service::get now lives at
axum::routing::service_method_routing::get, etc. (#405)Router::or renamed to Router::merge and will now panic on
overlapping routes. It now only accepts Routers and not general Services.
Use Router::fallback for adding fallback routes (#408)Router::fallback for adding handlers for request that didn't
match any routes. Router::fallback must be use instead of nest("/", _) (#408)EmptyRouter has been renamed to MethodNotAllowed as it's only
used in method routers and not in path routers (Router)CONNECT method. An
example of combining axum with and HTTP proxy can be found here (#428)i128 and u128 in extract::Pathextract::Path
(#272)Connected::connect_info to return Self and remove
the associated type ConnectInfo (#396)extract::MatchedPath for accessing path in router that
matched the request (#412)breaking: Simplify error handling model (#402):
error_handling module.Router::check_infallible has been removed since routers are always
infallible with the error handling changes.IntoResponse.With these changes handling errors from fallible middleware is done like so:
use axum::{
routing::get,
http::StatusCode,
error_handling::HandleErrorLayer,
response::IntoResponse,
Router, BoxError,
};
use tower::ServiceBuilder;
use std::time::Duration;
let middleware_stack = ServiceBuilder::new()
// Handle errors from middleware
//
// This middleware most be added above any fallible
// ones if you're using `ServiceBuilder`, due to how ordering works
.layer(HandleErrorLayer::new(handle_error))
// Return an error after 30 seconds
.timeout(Duration::from_secs(30));
let app = Router::new()
.route("/", get(|| async { /* ... */ }))
.layer(middleware_stack);
fn handle_error(_error: BoxError) -> impl IntoResponse {
StatusCode::REQUEST_TIMEOUT
}
And handling errors from fallible leaf services is done like so:
use axum::{
Router, service,
body::Body,
routing::service_method_routing::get,
response::IntoResponse,
http::{Request, Response},
error_handling::HandleErrorExt, // for `.handle_error`
};
use std::{io, convert::Infallible};
use tower::service_fn;
let app = Router::new()
.route(
"/",
get(service_fn(|_req: Request<Body>| async {
let contents = tokio::fs::read_to_string("some_file").await?;
Ok::<_, io::Error>(Response::new(Body::from(contents)))
}))
.handle_error(handle_io_error),
);
fn handle_io_error(error: io::Error) -> impl IntoResponse {
// ...
}
Document debugging handler type errors with "axum-debug" ([#372])
Bump minimum version of async-trait ([#370])
Clarify that handler::any and service::any only accepts standard HTTP methods ([#337])
Add accessors for TypedHeaderRejection fields ([#317])
Document using StreamExt::split with WebSocket ([#291])
fixed: Fix accidental breaking change introduced by internal refactor. BoxRoute used to be Sync but was accidental made !Sync
BoxRoute used to be Sync but was accidental made !Sync (#273)fixed: Fix URI captures matching empty segments. This means requests with URI / will no longer be matched by /:key
added: Add Redirect::to constructor
fixed: Overall compile time improvements. If you're having issues with compile time please file an issue!
Overall:
Routing:
Router to replace the RoutingDsl trait (#214)Router::or for combining routes (#108)Router::new().route("/", get(...)).route("/", post(...)) now
accepts both GET and POST. Previously only POST would be accepted (#224)get routes will now also be called for HEAD requests but will always have
the response body removed (#129)axum::route(...) with axum::Router::new().route(...). This means
there is now only one way to create a new router. Same goes for
axum::routing::nest. (#215)routing::MethodFilter via bitflags (#158)handle_error from ServiceExt to service::OnMethod (#160)With these changes this app using 0.1:
use axum::{extract::Extension, prelude::*, routing::BoxRoute, AddExtensionLayer};
let app = route("/", get(|| async { "hi" }))
.nest("/api", api_routes())
.layer(AddExtensionLayer::new(state));
fn api_routes() -> BoxRoute<Body> {
route(
"/users",
post(|Extension(state): Extension<State>| async { "hi from nested" }),
)
.boxed()
}
Becomes this in 0.2:
use axum::{
extract::Extension,
handler::{get, post},
routing::BoxRoute,
Router,
};
let app = Router::new()
.route("/", get(|| async { "hi" }))
.nest("/api", api_routes());
fn api_routes() -> Router<BoxRoute> {
Router::new()
.route(
"/users",
post(|Extension(state): Extension<State>| async { "hi from nested" }),
)
.boxed()
}
Extractors:
FromRequest default to being generic over body::Body (#146)std::error::Error for all rejections (#153)OriginalUri for extracting original request URI in nested services (#197)FromRequest for http::Extensions (#169)RequestParts::{new, try_into_request} public so extractors can be used outside axum (#194)FromRequest for axum::body::Body (#241)extract::UrlParams and extract::UrlParamsMap. Use extract::Path instead (#154)extractor_middleware now requires RequestBody: Default (#167)RequestAlreadyExtracted to an enum with each possible error variant (#167)extract::BodyStream is no longer generic over the request body (#234)extract::Body has been renamed to extract::RawBody to avoid conflicting with body::Body (#233)RequestParts changes (#153)
method new returns an &http::Methodmethod_mut new returns an &mut http::Methodtake_method has been removeduri new returns an &http::Uriuri_mut new returns an &mut http::Uritake_uri has been removedResponses:
Headers for easily customizing headers on a response (#193)Redirect response (#192)body::StreamBody for easily responding with a stream of byte chunks (#237)Body and BodyError types to IntoResponse. This is
required for returning responses with bodies other than hyper::Body from
handlers. See the docs for advice on how to implement IntoResponse (#86)tower::util::Either no longer implements IntoResponse (#229)This IntoResponse from 0.1:
use axum::{http::Response, prelude::*, response::IntoResponse};
struct MyResponse;
impl IntoResponse for MyResponse {
fn into_response(self) -> Response<Body> {
Response::new(Body::empty())
}
}
Becomes this in 0.2:
use axum::{body::Body, http::Response, response::IntoResponse};
struct MyResponse;
impl IntoResponse for MyResponse {
type Body = Body;
type BodyError = <Self::Body as axum::body::HttpBody>::Error;
fn into_response(self) -> Response<Self::Body> {
Response::new(Body::empty())
}
}
SSE:
response::sse::Sse. This implements SSE using a response rather than a service (#98)axum::sse. It has been replaced by axum::response::sse (#98)Handler using SSE in 0.1:
use axum::{
prelude::*,
sse::{sse, Event},
};
use std::convert::Infallible;
let app = route(
"/",
sse(|| async {
let stream = futures::stream::iter(vec![Ok::<_, Infallible>(
Event::default().data("hi there!"),
)]);
Ok::<_, Infallible>(stream)
}),
);
Becomes this in 0.2:
use axum::{
handler::get,
response::sse::{Event, Sse},
Router,
};
use std::convert::Infallible;
let app = Router::new().route(
"/",
get(|| async {
let stream = futures::stream::iter(vec![Ok::<_, Infallible>(
Event::default().data("hi there!"),
)]);
Sse::new(stream)
}),
);
WebSockets:
Message an enum (#116)WebSocket now uses Error as its error type (#150)Handler using WebSockets in 0.1:
use axum::{
prelude::*,
ws::{ws, WebSocket},
};
let app = route(
"/",
ws(|socket: WebSocket| async move {
// do stuff with socket
}),
);
Becomes this in 0.2:
use axum::{
extract::ws::{WebSocket, WebSocketUpgrade},
handler::get,
Router,
};
let app = Router::new().route(
"/",
get(|ws: WebSocketUpgrade| async move {
ws.on_upgrade(|socket: WebSocket| async move {
// do stuff with socket
})
}),
);
Misc
tower-log which exposes tower's log feature. (#218)body::BoxStdError with axum::Error, which supports downcasting (#150)EmptyRouter now requires the response body to implement Send + Sync + 'static' (#108)Router::check_infallible now returns a CheckInfallible service. This
is to improve compile times (#198)Router::into_make_service now returns routing::IntoMakeService rather than
tower::make::Shared (#229)tower::BoxError has been replaced with axum::BoxError (#229)future modules (#133)EmptyRouter, ExtractorMiddleware, ExtractorMiddlewareLayer,
and QueryStringMissing no longer implement Copy (#132)service::OnMethod, handler::OnMethod, and routing::Nested have new response future types (#157)Deprecate extract::UrlParams and extract::UrlParamsMap. Use extract::Path instead
/ (#91)pin-project-lite instead of pin-project (#95)http crate and hyper::Server (#110)Query and Form extractors giving bad request error when query string is empty. (#117)Path extractor. (#124)IntoResponse of (HeaderMap, T) and (StatusCode, HeaderMap, T) would ignore headers from T (#137)extract::UrlParams and extract::UrlParamsMap. Use extract::Path instead (#138)Implement Deref most extractors
Stream for WebSocket (#52)Sink for WebSocket (#52)Deref most extractors (#56)405 Method Not Allowed for unsupported method for route (#63)MissingExtension rejections (#72)tower::Services (#69)axum::body::box_body to converting an http_body::Body to axum::body::BoxBody (#69)axum::sse for Server-Sent Events (#75)- Misc readme fixes.
- Initial release.
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →