Skip to content

Non-Send gRPC handler futures can be hard to diagnose when using connectrpc-axum #17

Description

@FedorKiselev76

Summary

While migrating to axum 0.8 + tonic 0.14, I ran into a situation where a non-Send gRPC handler future was effectively hidden when using connectrpc-axum, but became immediately obvious when switching to direct Axum routing with post_service.

This is not a bug in connectrpc-axum, but the abstraction can make the real root cause harder to discover, especially for users migrating to newer Axum versions.

Root cause

The actual issue was in my tonic service implementation.

I was holding a tokio::sync::MutexGuard across an .await, which makes the handler future !Send.

Axum 0.8 correctly requires:

Service::Future: Send

Problematic pattern (old code)

#[derive(Clone)]
pub struct LocalAuth {
    issuer: Arc<JwtIssuer>,
    dbcache: Arc<tokio::sync::Mutex<DbCacheWrapper>>,
}

#[tonic::async_trait]
impl Auth for LocalAuth {
    async fn init(
        &self,
        req: Request<InitRequest>,
    ) -> Result<Response<InitResponse>, Status> {
        let uuid = req.into_inner().uuid;

        // ❌ MutexGuard is held across an await
        let client_id = {
            let mut dbc = self.dbcache.lock().await;
            dbc.get_by_uuid(&uuid).await?.customer_id
        };

        Ok(Response::new(InitResponse {
            /* ... */
        }))
    }
}

Holding a MutexGuard across .await causes the entire future returned by init() to become non-Send.

Why this was confusing with connectrpc-axum

When the service was wired using:

MakeServiceBuilder::new()
    .add_grpc_service(AuthServer::new(local_auth))
    .build();

the !Send future did not surface immediately at the integration point.

After switching to direct Axum integration:


Router::new()
    .route(
        "/auth.v1.Auth/{*wildcard}",
        post_service(AuthServer::new(local_auth)),
    )

Axum failed fast with a clear and direct error:

error[E0277]: the trait bound ...::Future: Send is not satisfied

This instantly pointed to the real issue.

Why this matters

Axum 0.8 enforces Send very strictly (and correctly)

Holding a MutexGuard across .await is a common async Rust pitfall

With connectrpc-axum, this issue may:

surface later

or appear in a less obvious place

This can make debugging harder for users migrating from older stacks

Suggestions (documentation / diagnostics)

This is not a request to change behavior, just a suggestion to improve developer experience.

Documentation note

It would be very helpful to explicitly mention that:

gRPC handlers must be Send

Holding tokio::sync::MutexGuard across .await will break this

This is especially important with axum 0.8+

Earlier diagnostics (if feasible)

If possible, adding earlier trait bounds or clearer diagnostics around:

Service::Future: Send

could help users discover this issue closer to where services are registered.

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions