What's new in axum 0.6.0-rc.1

August 23, 2022

Today, we're happy to announce [axum] version 0.6.0-rc.1. axum is an ergonomic and modular web framework built with tokio, tower, and hyper.

In 0.6 we've reworked some fundamental parts of axum to make things more type safe, less surprising, and easier to use. In this post I'd like to highlight a few of the most impactful changes and new features.

This also includes new major versions for [axum-core], [axum-extra], and [axum-macros].

Type safe State extractor

Previously the recommended way to share state with handlers was to use the Extension middleware and extractor:

use axum::{
    Router,
    Extension,
    routing::get,
};

#[derive(Clone)]
struct AppState {}

let state = AppState {};

let app = Router::new()
    .route("/", get(handler))
    // Add `Extension` as a middleware
    .layer(Extension(state));

async fn handler(
    // And extract our shared `AppState` using `Extension`
    Extension(state): Extension<AppState>,
) {}

However this wasn't type safe, so if you forgot .layer(Extension(...)) things would compile just fine but you'd get runtime errors when calling handler.

In 0.6 you can use the new State extractor which works similarly to Extension but is type safe:

use axum::{
    Router,
    extract::State,
    routing::get,
};

#[derive(Clone)]
struct AppState {}

let state = AppState {};

// Create the `Router` using `with_state`
let app = Router::with_state(state)
    .route("/", get(handler));

async fn handler(
    // And extract our shared `AppState` using `State`
    //
    // This will only compile if the type passed to `Router::with_state`
    // matches what we're extracting here
    State(state): State<AppState>,
) {}

State also supports extracting "substates". See the docs for more details.

While Extension still works we recommend users migrate to State, both because it is more type safe but also because it is faster.

Type safe extractor ordering

Continuing the theme of type safety, axum now enforces that only one extractor consumes the request body. In 0.5 this would compile just fine but fail at runtime:

use axum::{
    Router,
    Json,
    routing::post,
    body::Body,
    http::Request,
};

let app = Router::new()
    .route("/", post(handler).get(other_handler));

async fn handler(
    // This would consume the request body
    json_body: Json<serde_json::Value>,
    // This would also attempt to consume the body but fail
    // since it is gone
    request: Request<Body>,
) {}

async fn other_handler(
    request: Request<Body>,
    // This would also fail at runtime, even if the `AppState` extension
    // was set since `Request<Body>` consumes all extensions
    state: Extension<AppState>,
) {}

The solution was to manually ensure that you only use one extractor that consumes the request and that it was the last extractor. axum 0.6 now enforces this at compile time:

use axum::{
    Router,
    Json,
    routing::post,
    body::Body,
    http::Request,
};

let app = Router::new()
    .route("/", post(handler).get(other_handler));

async fn handler(
    // We cannot extract both `Request` and `Json`, have to pick one
    json_body: Json<serde_json::Value>,
) {}

async fn other_handler(
    state: Extension<AppState>,
    // `Request` must be extracted last
    request: Request<Body>,
) {}

This was done by reworking the FromRequest trait and adding a new FromRequestParts trait.

Run extractors from middleware::from_fn

middleware::from_fn makes it easy to write middleware using familiar async/await syntax. In 0.6 such middleware can also run extractors:

use axum::{
    Router,
    middleware::{self, Next},
    response::{Response, IntoResponse},
    http::Request,
    routing::get,
};
use axum_extra::extract::cookie::{CookieJar, Cookie};

async fn my_middleware<B>(
    // Run the `CookieJar` extractor as part of this middleware
    cookie_jar: CookieJar,
    request: Request<B>,
    next: Next<B>,
) -> Response {
    let response = next.run(request).await;

    // Add a cookie to the jar
    let updated_cookie_jar = cookie_jar.add(Cookie::new("foo", "bar"));

    // Add the new cookies to the response
    (updated_cookie_jar, response).into_response()
}

let app = Router::new()
    .route("/", get(|| async { "Hello, World!" }))
    .layer(middleware::from_fn(my_middleware));

Note that this cannot be used to extract State. See the docs for more details.

Nested routers with fallbacks

Router::nest allows you to send all requests with a matching prefix to some other router or service. However in 0.5 it wasn't possible for nested routers to have their own fallbacks. In 0.6 that now works:

use axum::{Router, Json, http::StatusCode, routing::get};
use serde_json::{Value, json};

let api = Router::new()
    .route("/users/:id", get(|| async {}))
    // We'd like our API fallback to return JSON
    .fallback(api_fallback);

let app = Router::new()
    .nest("/api", api)
    // And our top level fallback to return plain text
    .fallback(top_level_fallback);

async fn api_fallback() -> (StatusCode, Json<Value>) {
    let body = json!({
        "status": 404,
        "message": "Not Found",
    });
    (StatusCode::NOT_FOUND, Json(body))
}

async fn top_level_fallback() -> (StatusCode, &'static str) {
    (StatusCode::NOT_FOUND, "Not Found")
}

Trailing slash redirects removed

Previously if you had a route for /foo but got a request with /foo/, axum would send a redirect response to /foo. However many found this behavior surprising and it had edge-case bugs when combined with services that did their own redirection, so in 0.6 we decided to remove this feature.

The recommended solution is to explicitly add the routes you want:

use axum::{
    Router,
    routing::get,
};

let app = Router::new()
    // Send `/foo` and `/foo/` to the same handler
    .route("/foo", get(foo))
    .route("/foo/", get(foo));

async fn foo() {}

If you want to opt into the old behavior you can use RouterExt::route_with_tsr

Mix wildcard routes and regular routes

axum's Router now has better support for mixing wilcard routes and regular routes:

use axum::{Router, routing::get};

let app = Router::new()
    // In 0.5 these routes would be considered overlapping and not be allowed
    // but in 0.6 it just works
    .route("/foo/*rest", get(|| async {}))
    .route("/foo/bar", get(|| async {}));

See the changelog for more

I encourage you to read the changelog to see all the changes and for tips on how to update from 0.5 to 0.6.0-rc.1.

Also, please ask questions in Discord or file issues if you have trouble updating or discover bugs. If everything goes smoothly we expect to ship 0.6.0 in a few weeks.

ā€” David Pedersen (@davidpdrsn)