teimuraz / tonic-middleware

Async middleware and interceptor for Tonic gRPC services
https://crates.io/crates/tonic-middleware
MIT License
50 stars 1 forks source link

Update to latest tonic #11

Closed feelingsonice closed 2 months ago

feelingsonice commented 2 months ago

Getting some breaking changes when updating to tonic 0.12.

teimuraz commented 2 months ago

Thanks @feelingsonice for reporting! Updated. New version: 0.2.0
tonic-middleware 0.2.x - will follow tonic 0.12.x versions

Breaking changes (due to tonic breaking changes)

Changed signature of interceptor function:

Instead of

async fn intercept(&self, req: Request<Body>) -> Result<Request<Body>, Status>

Use

use use tonic::body::BoxBody;
...

async fn intercept(&self, req: Request<BoxBody>) -> Result<Request<BoxBody>, Status>

Changed signature of middleware function:

Instead of

async fn intercept_with_endpoint_error(
    &self,
    mut req: Request<Body>,
) -> Result<Request<Body>, Status>

Use

use use tonic::body::BoxBody;
...

async fn intercept_with_endpoint_error(
    &self,
    mut req: Request<BoxBody>,
) -> Result<Request<BoxBody>, Status>
drauschenbach commented 2 months ago

I'm struggling to get this to work.

use tonic::body::BoxBody;
use tonic::transport::Channel;
use tonic::{async_trait, Code, Request, Status};
use tonic_middleware::RequestInterceptor;

#[derive(Clone)]
pub struct AuthInterceptor {
    pub my_channel: Channel,
}

#[async_trait]
impl RequestInterceptor for AuthInterceptor {
    async fn intercept(&self, req: Request<BoxBody>) -> Result<Request<BoxBody>, Status> {
        Ok(req)
    }
}
error[E0053]: method `intercept` has an incompatible type for trait
  --> my_server/src/authorize.rs:15:36
   |
15 |     async fn intercept(&self, req: Request<BoxBody>) -> Result<Request<BoxBody>, Status> {
   |                                    ^^^^^^^^^^^^^^^^
   |                                    |
   |                                    expected `Request<UnsyncBoxBody<..., ...>>`, found a different `Request<UnsyncBoxBody<..., ...>>`
   |                                    help: change the parameter type to match the trait: `request::Request<http_body_util::combinators::box_body::UnsyncBoxBody<prost::bytes::Bytes, tonic::Status>>`
   |
   = note: expected signature `fn(&'life0 AuthInterceptor, request::Request<http_body_util::combinators::box_body::UnsyncBoxBody<prost::bytes::Bytes, tonic::Status>>) -> Pin<Box<(dyn Future<Output = Result<request::Request<http_body_util::combinators::box_body::UnsyncBoxBody<prost::bytes::Bytes, tonic::Status>>, tonic::Status>> + Send + 'async_trait)>>`
              found signature `fn(&'life0 AuthInterceptor, tonic::Request<http_body_util::combinators::box_body::UnsyncBoxBody<prost::bytes::Bytes, tonic::Status>>) -> Pin<Box<(dyn Future<Output = Result<tonic::Request<http_body_util::combinators::box_body::UnsyncBoxBody<prost::bytes::Bytes, tonic::Status>>, tonic::Status>> + Send + 'async_trait)>>`

For more information about this error, try `rustc --explain E0053`.
feelingsonice commented 2 months ago

@drauschenbach could you post your cargo.toml?

drauschenbach commented 2 months ago
[workspace.dependencies]
anyhow = "1.0.86"
async-stream = "0.3.5"
env_logger = { version = "0.11.3", features = ["unstable-kv"] }
itertools = "0.13.0"
log = { version = "0.4.21", features = ["kv_unstable_serde", "kv_unstable"] }
pbjson = "0.7.0"
pbjson-types = "0.7.0"
prost = "0.13.1"
prost-build = "0.13.1"
prost-types = "0.13.1"
rustls = "0.23.11"
serde = { version = "1.0.202", features = ["derive"] }
serde_json = "1.0.116"
structopt = "0.3.26"
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] }
tokio-stream = "0.1.15"
tonic = { version = "0.12.0", features = ["tls", "tls-roots"] }
tonic-middleware = "0.2.0"
teimuraz commented 2 months ago

@drauschenbach You need to use use tonic::codegen::http::Request; instead of use tonic::Request;

use tonic::body::BoxBody;
use tonic::transport::Channel;
use tonic::{async_trait, Status};
use tonic_middleware::RequestInterceptor;
use tonic::codegen::http::Request;

#[derive(Clone)]
pub struct AuthInterceptor {
    pub my_channel: Channel,
}

#[async_trait]
impl RequestInterceptor for AuthInterceptor {
    async fn intercept(&self, req: Request<BoxBody>) -> Result<Request<BoxBody>, Status> {
        Ok(req)
    }
}
drauschenbach commented 2 months ago

tonic::codegen::http::Request doesn't have a metadata accessor (that I can find?), so is unusable for gRPC middleware.

teimuraz commented 2 months ago

@drauschenbach Pls check example, instead of metadata, you can use

    // Set user id in header, so it can be used in grpc services through tonic::Request::metadata()
    let user_id_header_value = HeaderValue::from_str(&user_id.to_string())
        .map_err(|_e| Status::internal("Failed to convert user_id to header value"))?;
    req.headers_mut().insert("user_id", user_id_header_value);
    Ok(req)

Unfortunately, due to nature of tonic implementation, there is no way to directly use/consume tonic::Request in interceptors in async manner (unless directly modifying tonic source code, at least I did not found)

Full example is here: https://github.com/teimuraz/tonic-middleware/blob/main/example/src/server.rs#L67

Though there are examples and integration tests, probably I need to update README to make it more explicit and clear

feelingsonice commented 2 months ago

using http::Request from the http crate version 1.1 works for me

teimuraz commented 2 months ago

using http::Request from the http crate version 1.1 works for me

Yeah, tonic::codegen::http::Request is just basically re-export of http::Request

teimuraz commented 2 months ago

Updated README with proper imports. Hopefully, this will help reduce ambiguity https://github.com/teimuraz/tonic-middleware?tab=readme-ov-file#note

drauschenbach commented 2 months ago

I'm trying to access gRPC metadata, not mutate it.

async fn intercept(&self, req: Request<BoxBody>) -> Result<Request<BoxBody>, Status> {
        // Parse metadata for a bearer token
        let bearer_token = match req.metadata().get("authorization") {
            Some(authorization) => {
                let authorization = authorization
                    .to_str()
                    .map_err(|e| Status::new(Code::InvalidArgument, e.to_string()))?;
                match authorization.strip_prefix("Bearer ") {
                    Some(token) => token.to_string(),
                    _ => {
                        return Err(Status::unauthenticated(
                            "Unsupported authorization mechanism; use Bearer",
                        ))
                    }
                }
            }
            _ => return Err(Status::unauthenticated("An authorization header is required")),
        };

        // Lookup a matching API Key
        ...
drauschenbach commented 2 months ago

Is gRPC metadata nothing more than another name for http headers?

feelingsonice commented 2 months ago

@drauschenbach I think they're the same in the context of tonic.

You can get the bearer token with something like:

match request.headers().get("authorization").map(|v| v.to_str()) {
    Some(Ok(raw)) => {
        let token_str = raw.strip_prefix("Bearer ").ok_or(Status::invalid_argument(
            "Authorization header missing 'Bearer ' prefix",
        ))?;
...
teimuraz commented 2 months ago

Is gRPC metadata nothing more than another name for http headers?

Basically yes, metadata is wrapper around headers. From tonic source code:

#[derive(Clone, Debug, Default)]
pub struct MetadataMap {
    headers: http::HeaderMap,
}