Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add middleware::from_fn for creating middleware from async fns #656

Merged
merged 8 commits into from
Dec 27, 2021

Conversation

davidpdrsn
Copy link
Member

@davidpdrsn davidpdrsn commented Dec 27, 2021

I feel axum is in a pretty good place in terms of ease of use. However one area that is still a big hurdle for some people is tower::Service which quickly comes up up when writing custom middleware. Suddenly you cannot use high level async/await syntax anymore and have to worry about pinning, poll_ready, and most likely lots of generics.

This is something I've been thinking about for awhile but hadn't found a good solution for. But today I discovered poem's middleware_fn which I think is perfect for this!

So this PR introduces axum_extra::middleware::from_fn that works like this:

use axum::{
    Router,
    http::{Request, StatusCode},
    routing::get,
    response::IntoResponse,
};
use axum_extra::middleware::{self, Next};

async fn auth<B>(req: Request<B>, next: Next<B>) -> impl IntoResponse {
    let auth_header = req.headers().get(http::header::AUTHORIZATION);

    match auth_header {
        Some(auth_header) if auth_header == "secret" => {
            Ok(next.run(req).await)
        }
        _ => Err(StatusCode::UNAUTHORIZED),
    }
}

let app = Router::new()
    .route("/", get(|| async { /* ... */ }))
    .route_layer(middleware::from_fn(auth));

You have pretty much the same level of control as with tower::Service but with a higher level API.

@davidpdrsn davidpdrsn added C-enhancement Category: A PR with an enhancement A-axum-extra labels Dec 27, 2021
Comment on lines +156 to +158
pub struct Next<ReqBody> {
inner: BoxCloneService<Request<ReqBody>, Response, Infallible>,
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also considered calling this Rest but went with Next since its more common across other frameworks such as tide.

@davidpdrsn
Copy link
Member Author

davidpdrsn commented Dec 27, 2021

Also worth noting that for simple middleware tower's map_request, map_response, etc, utilities gets you must of the way there but its not easy to write something like the auth middleware shown in the PR description without a custom middleware since you need to return early.

extractor_middleware would also work but that has other limitations that middleware::from_fn doesn't have, such as access to the response.

axum-extra/src/middleware/middleware_fn.rs Show resolved Hide resolved
Comment on lines +107 to +117
where
F: FnMut(Request<ReqBody>, Next<ReqBody>) -> Fut,
Fut: Future<Output = Out>,
Out: IntoResponse,
S: Service<Request<ReqBody>, Response = Response<ResBody>, Error = Infallible>
+ Clone
+ Send
+ 'static,
S::Future: Send + 'static,
ResBody: HttpBody<Data = Bytes> + Send + 'static,
ResBody::Error: Into<BoxError>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's quite a long list of constraints ^^

I wonder what cursed middleware one could write that actually makes use of the extra capabilities FnMut gives in addition to "plain" Fn 😅

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hehe yeah 😜 I'm not sure FnMut is that useful in practice actually but since we a &mut self I figure why not. Tower also uses FnMut.

axum-extra/src/middleware/middleware_fn.rs Outdated Show resolved Hide resolved
axum-extra/src/middleware/mod.rs Outdated Show resolved Hide resolved
@davidpdrsn davidpdrsn enabled auto-merge (squash) December 27, 2021 12:42
@davidpdrsn davidpdrsn merged commit f471608 into main Dec 27, 2021
@davidpdrsn davidpdrsn deleted the middleware-from-fn branch December 27, 2021 13:01
davidpdrsn added a commit that referenced this pull request Dec 27, 2021
- Add `middleware::from_fn` for creating middleware from async functions ([#656])

[#656]: #656
davidpdrsn added a commit that referenced this pull request Dec 27, 2021
- Add `middleware::from_fn` for creating middleware from async functions ([#656])

[#656]: #656
davidpdrsn added a commit that referenced this pull request Dec 27, 2021
- Add `middleware::from_fn` for creating middleware from async functions ([#656])

[#656]: #656
@johannescpk
Copy link
Contributor

johannescpk commented Jan 12, 2022

Probably a naive question, but how would one pass some context variables into the middleware function? I've tried wrapping the middleware itself into a "factory function", but failed to do so because of the B generics requirement. Basically something like

pub fn auth(some_context_variable: Context) -> impl Fn(Request<_>, Next<_>) -> impl IntoResponse {
    |req: Request<_>, next: Next: <_>| async move {
        // do something based on `some_context_variable`
        Ok(next.run(req).await)
    }
}

@davidpdrsn
Copy link
Member Author

@johannescpk You can do this:

use axum::{http::Request, response::IntoResponse, Router};
use axum_extra::middleware::{self, Next};
use std::net::SocketAddr;

#[derive(Clone)]
struct Context {}

#[tokio::main]
async fn main() {
    let ctx = Context {};

    let app = Router::new().layer(middleware::from_fn(move |req, next| {
        foo(ctx.clone(), req, next)
    }));

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn foo<B>(ctx: Context, req: Request<B>, next: Next<B>) -> impl IntoResponse {
    next.run(req).await
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-axum-extra C-enhancement Category: A PR with an enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants