Axum
Axum is a web framework based on hyper and tokio runtime.
Overview
Axum uses tower::Service to handle the HTTP requests,
which takes full advantage of tower and tower-http crate.
Therefore all you need to write is how to process http::Request<B> and return http::Response<B>.
Note that this service must not return error, therefore its error type is Infallible.
By default, all the services in axum are intended to be one-time use, therefore always ready to be called.
Our ultimate goal is to provide MakeService to TcpListener (via serve function),
so that whenever Request arrives, creates Service to process request and return response.
Internally Serve is a hyper server.
Usually the service is created by Router, which is a layered service that maps the paths and services.
Router is then wrapped into IntoMakeService, which is a simple MakeService that clones the Router.
Router is Clone since it wraps internal structure with Arc.
1
2
3
4
5
6
7
8
#[tokio::main]
async fn main() {
let listener = TcpListener::bind(address).await.unwrap();
let router = router();
axum::serve(listener, router.into_make_service()).await.unwrap();
}
fn router() -> Router { /* ... */ }
Body
Axum uses its own default Body type which is internally a boxed Body trait object of that is !Sync.
Its data type is Bytes and error type is its own default Error type which is also a boxed Error trait object.
To distinguish with Body trait, axum renames Body trait as HttpBody.
Request and Response in axum also uses this type as their default body,
yet axum can still handle Request with arbitrary HttpBody which is compatible with this type.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pub struct Body(UnsyncBoxBody<Bytes, Error>);
pub struct UnsyncBoxBody<D, E> {
inner: Pin<Box<dyn Body<Data = D, Error = E> + Send + 'static>>,
}
pub struct Error {
inner: Box<dyn std::error::Error + Send + Sync>,
}
impl Body {
/// Convert arbitrary body type compatible with this type.
/// That is, boxable and its data type is `Bytes` and error type is standard Error.
fn new<B>(body: B) -> Self
where
B: HttpBody<Data = Bytes> + Send + 'static,
B::Error: Into<BoxError>, // Equivalent to `B::Error: std::error::Error + Send + Sync`
{ /* ... */ }
}
Components
Axum framework consists of:
- Routing
- Handlers
- Extractors
- Responses
- Middleware
Routing
Router is used to set up which path goes to which services.
Unless you add additional Routes this will respond with 404 Not Found to all requests.
Each Route is a Service which is also Clone + Send + 'static.
They are internal types.
1
pub struct Router<S = ()> { /* private fields */ }
State
Its generic type S is a type of shared state that must be provided to handlers (not provided yet).
This type must be Clone + Send + Sync + 'static.
Router can also add Handler, which is similar to Service but requires state to process request.
Once state is provided to Handlers in Router via with_state method,
Handlers are converted into Routes and they cannot accept any other states.
By providing state S, we can convert S into any other type.
Unless S is unit, Router is not a Service,
since Handlers require state to be provided to process request.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
impl<S: Clone + Send + Sync + 'static> Router<S> {
/// The state is applied to all `Handler`s in the router.
fn with_state<S2>(self, state: S) -> Router<S2> { /* ... */ }
}
impl Router<()> {
fn into_make_service(self) -> IntoMakeService<Self> { /* ... */ }
/// `MakeService` with `ConnectInfo` as extension.
fn into_make_service_with_connect_info<C>(self) -> IntoMakeServiceWithConnectInfo<Self, C> { /* ... */ }
}
impl<B> Service<Request<B>> for Router<()>
where
B: HttpBody<Data = Bytes> + Send + 'static,
B::Error: Into<BoxError>, // Equivalent to `B::Error: std::error::Error + Send + Sync`
{
type Response = Response;
type Error = Infallible;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request<B>) -> Self::Future {
self.call_with_state(req.map(Body::new), ())
}
}
Inner Structure
Internally, Router is composed of path_router, fallback_router, catch_all_fallback.
path_router and fallback_router are both PathRouter, which maps each path to Route or Handler.
catch_all_fallback is Route or Handler whose error type is Infallible, working as a global fallback.
Unless you set fallback by fallback method,
default fallback_router and catch_all_fallback will respond with 404 NOT FOUND to all requests.
When the request arrives, Router first tries to process with path_router,
then falls to fallback_router on failure, and finally catch_all_fallback.
PathRouter is an internal structure that maps path to Route or MethodRouter.
MethodRouter is another internal structure that maps HTTP method to Route or Handler.
Default MethodRouter will respond with 405 Method Not Allowed to all requests.
You can create MethodRouter using method filter and Handler or Service (as Route).
The Service must be Clone so that it can be Route.
Implementation
route: MapsMethodRouterto given path.route_service: MapsService(asRoute) to given path. TheServicemust beCloneso that it can beRoute.nest: Nest anotherRouterat given path. Note that it does not inheritcatch_all_fallback.fallback_routerwill also be nested unless it is a default router.merge: Merges the paths and fallbacks of two router.layer: Apply atower::Layerto allRoutes andHandlers in router. This also includesfallback_router.route_layer: Apply atower::Layerto allRoutes andHandlers only inpath_router.fallback: Add aHandleras a fallback. This sets bothfallback_routerandcatch_all_fallback.fallback_service: Add aService(asRoute) as a fallback. TheServicemust beCloneso that it can beRoute. This sets bothfallback_routerandcatch_all_fallback.
Handlers
As mentioned above, Handler is similar to Service but it also requires state to process request.
Handler::with_state will convert Handler into Service.
Simply it is async fn<S>(Request, S) -> Response.
The beauty of axum is writing Handler as an asynchronous function(or closure) whose parameters are extractors(extractable from request or state).
Axum provides blanket implementations for functions that:
- (Function) It must be
Clone + Send + 'static. - (Parameters) Take no more than 16 arguments that all implement Send.
- All except the last argument implement
FromRequestParts. - The last argument implements either
FromRequestPartsorFromRequest.
- All except the last argument implement
- (Return) Returns a future that is
Send. The most common way to accidentally make a future!Sendis to hold a!Sendtype across an await. You must care the parameters to beSendorSync(reference) to make a futureSend. - (Return) Future returns something that implements
IntoResponse.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pub trait Handler<T, S>: Clone + Send + Sized + 'static {
type Future: Future<Output = Response> + Send + 'static;
// Required method
fn call(self, req: Request, state: S) -> Self::Future;
// Provided methods
// Still Handler (impl Handler for Layered)
fn layer<L>(self, layer: L) -> Layered<L, Self, T, S>
where
L: Layer<HandlerService<Self, T, S>> + Clone,
L::Service: Service<Request>
{ ... }
// Service (impl Service for HandlerService)
fn with_state(self, state: S) -> HandlerService<Self, T, S> { ... }
}
Functions w/ Generic Types
Dealing with generic-typed functions is much more complicated. There are more rules to follow.
- (Function) Since the function must be
Clone + Send + 'static, all generic types must always be'static. Hopefully, functions are alwaysCloneandSend.
Extractors
Usually a handler function is an async function that takes any number of “extractors” as arguments.
An extractor is a type that implements FromRequest or FromRequestParts.
Extractors are how you pick apart the incoming request to get the parts your handler needs.
For example, Json is an extractor that consumes the request body and deserializes it as JSON into some target type.
Extractors always run in the order of the function parameters that is from left to right.
The request body is an asynchronous stream that can only be consumed once. Therefore you can only have one extractor that consumes the request body. Axum enforces this by requiring such extractors to be the last argument your handler takes.
Common Extractors
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// `Path` gives you the path parameters and deserializes them.
async fn path(Path(user_id): Path<u32>) {}
// `Query` gives you the query parameters and deserializes them.
async fn query(Query(params): Query<HashMap<String, String>>) {}
// `HeaderMap` gives you all the headers
async fn headers(headers: HeaderMap) {}
// `String` consumes the request body and ensures it is valid utf-8
async fn string(body: String) {}
// `Bytes` gives you the raw request body
async fn bytes(body: Bytes) {}
// `Json` comsumes the request body and deserializes it as JSON into some target type
async fn json(Json(payload): Json<Value>) {}
// `Request` gives you the whole request for maximum control
async fn request(request: Request) {}
// `Extension` extracts data from "request extensions"
// This is commonly used to share state with handlers
async fn extension(Extension(state): Extension<State>) {}
Optional Extractors
All extractors defined in axum will reject the request if it doesn’t match.
If you wish to make an extractor optional you can wrap it in Option.
Request Body Limits
For security reasons, Bytes will, by default, not accept bodies larger than 2MB.
This also applies to extractors that uses Bytes internally such as String, Json, and Form.
Responses
Anything that implements IntoResponse can be returned from a handler.
Axum provides implementations for common types.
In general you can return tuples like:
- (StatusCode, impl IntoResponse)
- (Parts, impl IntoResponse)
- (Response<()>, impl IntoResponse)
- (T1, .., Tn, impl IntoResponse) where T1 to Tn all implement IntoResponseParts.
- (StatusCode, T1, .., Tn, impl IntoResponse) where T1 to Tn all implement IntoResponseParts.
- (Parts, T1, .., Tn, impl IntoResponse) where T1 to Tn all implement IntoResponseParts.
- (Response<()>, T1, .., Tn, impl IntoResponse) where T1 to Tn all implement IntoResponseParts.