rustrust-axumrust-tonic

Multiplex axum and tonic on the same listener?


I have axum and tonic running on the same server like the following:

let state = AppState {
        rate_limiter: Arc::new(RateLimiter::new(10, Duration::from_secs(60))), // 10 requests per minute
    };

    let greeter_service = grpc::hello_world::MyGreeter::default();
    let grpc_service =
        Server::builder().add_service(greeter_server::GreeterServer::new(greeter_service));

    let app = axum::Router::new()
        // ... many .route()
        .layer(
            ServiceBuilder::new()
                .layer(middleware::from_fn(trace_http))
                // https://github.com/tokio-rs/axum/discussions/987
                .layer(HandleErrorLayer::new(|err: BoxError| async move {
                    // turns layer errors into HTTP errors
                    error!("Unhandled error: {}", err);
                    (
                        StatusCode::INTERNAL_SERVER_ERROR,
                        format!("Unhandled error: {}", err),
                    )
                }))
                .layer(BufferLayer::new(1024))
                .layer(DefaultBodyLimit::max(1_000_000))
                // also see https://docs.rs/tower-http/0.6.1/tower_http/request_id/index.html#example
                .layer(tower::timeout::TimeoutLayer::new(Duration::from_secs(60))) // 30 second timeout
                .layer(middleware::from_fn_with_state(
                    state.clone(),
                    ip_rate_limiter,
                )),
        )
        .with_state(state);

    info!("Starting on http://{} and grpc://{}", http_addr, grpc_addr);
    let axum_listener = tokio::net::TcpListener::bind(http_addr).await.unwrap();

    let axum_server = axum::serve(axum_listener, app).with_graceful_shutdown(async {
        tokio::signal::ctrl_c()
            .await
            .expect("Failed to install Ctrl+C handler");
        info!("Received shutdown signal");
    });
    let grpc_server = grpc_service.serve_with_shutdown(grpc_addr, async {
        tokio::signal::ctrl_c()
            .await
            .expect("Failed to install Ctrl+C handler");
        info!("Received shutdown signal");
    });

    _ = tokio::join!(axum_server, grpc_server);

Right now they run on different ports. Is there a way to have them multiplexed on the same port for http 1.1/h2c support such that I can route an incoming request to the correct service based on the header (i.e. if application/grpc send to tonic, otherwise axum)

tonic = "0.12.3"
axum = { version = "0.8.1", features = ["macros"] }

current versions used, can upgrade as needed


Solution

  • Tonic has a Routes type that you can add services to and then finish with .into_axum_router() to use it with axum.

    This is pretty much what I've done before using this tonic-made router as the base:

    let app = Routes::default()
        .add_service(greeter_server::GreeterServer::new(greeter_service))
        .add_service(...)
        .add_service(...)
        .into_axum_router()
        .with_state(()) // needed to use routes with a different state
        .route(...)
        .route(...)
        .route(...)
        .layer(...)
        .with_state(state);
    

    This requires tonic's router Cargo feature, but it is enabled by default.