How to encapsulate a builder that depends on a closure

I'm working with Axum. In order to build the app, I need to instantiate service layers and routes.

I'm trying to create a module that encapsulates the middleware stack.

#[tokio::main]
async fn main() {
    // retrieve the config options
    let opt = Options::parse();
    ...
   
    let store = MemoryStore::new();
    let oauth_client = oauth_client::init();
    let middleware_stack = middleware::stack::init();

    let app = routes::init()
        .layer(middleware_stack)
        .layer(AddExtensionLayer::new(store))
        .layer(AddExtensionLayer::new(oauth_client));

    let addr = std::net::IpAddr::from_str(opt.addr.as_str())
        .unwrap_or_else(|_| "127.0.0.1".parse().unwrap());
    let sock_addr = SocketAddr::from((addr, opt.port));

    tracing::info!("listening on http://{}", sock_addr);

    axum::Server::bind(&sock_addr)
        .serve(app.into_make_service_with_connect_info::<SocketAddr, _>())
        .await
        .expect("Server failed");
}

The module that specifies the middleware:

type Stack<T> = Stack<TraceLayer<SharedClassifier<ServerErrorsAsFailures>, T>, Identity>;

pub fn init<T>() -> ServiceBuilder<Stack<T>> {
    ServiceBuilder::new().layer(TraceLayer::new_for_http().make_span_with(
        |request: &Request<Body>| {
            let ConnectInfo(addr) = request
                .extensions()
                .get::<ConnectInfo<SocketAddr>>()
                .unwrap();
            let empty_val = &HeaderValue::from_static("");
            let user_agent = request
                .headers()
                .get("User-Agent")
                .unwrap_or(empty_val)
                .to_str()
                .unwrap_or("");
            tracing::debug_span!("client-addr", addr = %addr, user_agent=%user_agent)
        },
    ))
}

T is the closure. I understand that I cannot express the type for the closure. So, clearly my approach is flawed.

What might be a better way to build and assemble the app without jamming all of the logic into main?

Thank you in advance for any guidance.

- E

That's not even the problem. The problem is that you are trying to use the generic parameter as an output type. That's not what generics are for.

A generic parameter is an input to a function (or generic type). If you write fn init<T>(), then a caller can invoke it like init::<u32>() or init::<String>() or init::<()>() or however s/he wants.

What you are looking for here is an output type "parameter", i.e. a way to specify that a function returns a function-like value, but it can't be named exactly. That's what impl Trait is for. The signature you likely need is:

fn init() -> ServiceBuilder<Stack<impl FnMut(&Request<Body>) -> Ret>>

(where Ret is the return type of the closure, I can't tell what it is.)

Speaking from experience, breaking up the code like that is very hard due to all the generics. I would instead recommend functions that accept and return axum::Routers. That way you bypass all the generics:

#[tokio::main]
async fn main() {
    let mut app = Router::new().route("/todos", get(|| async {}));

    app = add_middleware(app);
    app = add_more_middleware(app);

    // ...
}

fn add_middleware(router: Router) -> Router {
    router.layer(
        ServiceBuilder::new()
            .layer(TraceLayer::new_for_http())
            .layer(AddExtensionLayer::new(State { ... })),
    )
}

fn add_more_middleware(router: Router) -> Router {
    router.layer(ServiceBuilder::new().layer(...))
}

Thank you. That was clarifying.

I was using

type Go<T> =
    ServiceBuilder<Stack<TraceLayer<SharedClassifier<ServerErrorsAsFailures>, T>, Identity>>;

... to help understand/build the type.

Using the return of a trait object:

pub fn init() -> Go<impl FnMut(&Request<Body>) -> Span> {...}

in the code compiled as you suggested. Thank you. Per usual, I clarified my understanding with your response.

My approach was awkward (at best) given the likely intent of using Router (see next response).

One nit: impl Trait is not called a "trait object" (that would be dyn Trait, which is an entirely different mechanism for achieving similar results).

1 Like

Wow. That's an important "nit" to me. I thought I had that understanding licked. Is there type erasure when I can only commit to a type that implements a trait as the return value?

There's type opaqueness with return-position impl Trait -- meaning that outside of the defining body, code can only count on the type implementing the declared bounds (mostly [1]). But there's no type erasure -- the compiler still knows what the underlying type is, it monomorphizes code based on that underlying type, method calls do not involve a new layer of vtable indirection, etc. When Rust gets specialization, they will also keep their specializations.

As with type erasure, the opaqueness allows the returning of unnameable types, and provides the flexibility to the defining body of being able to change the underlying types without breaking other code [2]. Additionally, the trait bounds need not be dyn-safe, and other restrictions around Sized are avoided. However, the lack of type erasure is also why you cannot return different underlying types in different branches with impl Trait type opaqueness alone. Additionally, code outside of the defining body cannot name the opaque types in stable Rust, while it can of course name a dyn Trait (though there are plans to restore this ability indirectly in the future, e.g. TAIT -- type alias impl Trait).

You can read more in the RFC. Note that the RFC predates the dyn Trait syntax, so trait objects are just referred to as Trait in places.


  1. auto traits like Send and Sync leak through the opaqueness boundary ↩︎

  2. modulo the aforementioned leaky auto traits ↩︎

3 Likes

Right... thank you for that. If memory serves, what makes it possible to host a collection of different types is using dyn Trait behind a ref such as & or <Box>. This indirection is not required when returning a type specified by trait bounds because we remain confined to a single concrete type (opaque, but of one concrete type).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.