2026-08-23
The asker wants a HashMap of route paths to async handlers. They've fallen into the classic pit: async fn in a trait desugars to impl Future, which isn't object-safe in the naive form, so they've reached for dyn_clone, Any::downcast_mut, and hand-rolled Pin<Box<dyn Future>> to make everything fit behind a dyn pointer.
Why it's genuinely hard: Rust's async model is zero-cost — each async fn produces a unique anonymous Future type. Erasing that into a uniform handler type requires boxing the future somewhere. The question is where to hide the box so callers don't see it. Meanwhile, handlers have different argument shapes (path params, query, JSON body, state), so a "one signature fits all" trait tends to devolve into Box<dyn Any> plumbing — which is exactly what the asker is trying to escape.
The clean approach mainstream frameworks use (axum, actix) is the extractor pattern combined with a blanket-impl Handler trait:
trait Handler<Args>: Clone + Send + 'static {
type Future: Future<Output = Response> + Send;
fn call(self, req: Request) -> Self::Future;
}
// Blanket impl for every async fn shape you support
impl<F, Fut, A1> Handler<(A1,)> for F
where
F: Fn(A1) -> Fut + Clone + Send + 'static,
Fut: Future<Output = Response> + Send,
A1: FromRequest,
{ /* extract A1, call self, return Fut */ }
Then wrap concrete handlers in a small erased type at registration time:
type BoxedHandler = Arc<dyn Fn(Request) -> BoxFuture<'static, Response> + Send + Sync>;
fn erase<H, A>(h: H) -> BoxedHandler
where H: Handler<A>, A: FromRequest + 'static
{
Arc::new(move |req| Box::pin(h.clone().call(req)))
}
The HashMap<String, BoxedHandler> then holds uniform values. No Any, no dyn_clone, no downcasting — the type erasure happens exactly once at insertion, and the boxing cost is one allocation per request (unavoidable for dynamic dispatch).
Gotchas:
async fn return types capture lifetimes of all arguments — use BoxFuture<'static, T> and move owned values into the future, or the borrow checker will drag you back into Pin hell.impl_handler! macro expansions for 0..16 args.Send bounds are viral — miss one and error messages become opaque walls of impl Future.Clone on handlers, Arc<F> at the erasure boundary is cheaper and simpler than dyn_clone.