├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src └── lib.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-22.04 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose --release 21 | - name: Run tests 22 | run: cargo test --verbose --release 23 | - name: Check formatting 24 | run: cargo fmt --check --verbose 25 | - name: Check Lint 26 | # Fail on clippy warnings (-D warnings), and run clippy on tests too (--all-targets): 27 | run: cargo clippy --all-targets -- -D warnings 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | 4 | # Backup files generated by rustfmt 5 | **/*.rs.bk 6 | 7 | # emacs temp files 8 | *~ 9 | 10 | # Jetbrains project files 11 | .idea 12 | *.iml 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tonic-async-interceptor" 3 | authors = ["Joe Dahlquist "] 4 | categories = ["web-programming", "network-programming", "asynchronous", "authentication"] 5 | description = "Async variant of Tonic's interceptor function" 6 | documentation = "https://docs.rs/tonic-async-interceptor" 7 | edition = "2021" 8 | homepage = "https://github.com/arcanyx-pub/tonic-async-interceptor" 9 | keywords = ["async", "grpc", "tonic", "protobuf", "interceptor"] 10 | license = "MIT" 11 | readme = "README.md" 12 | repository = "https://github.com/arcanyx-pub/tonic-async-interceptor" 13 | version = "0.13.0" 14 | 15 | [dependencies] 16 | bytes = "1.0.0" 17 | http = "1.1.0" 18 | http-body = "1.0.0" 19 | pin-project = "1.1.0" 20 | tonic = "0.13.0" 21 | tower-layer = "0.3.0" 22 | tower-service = "0.3.0" 23 | 24 | [dev-dependencies] 25 | http-body-util = "0.1.2" 26 | tokio = {version = "1.38.0", features = ["rt", "macros"]} 27 | tower = {version = "0.5.0", features = ["full"]} 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Arcanyx Technical Wizardry LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tonic Async Interceptor 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/tonic-async-interceptor)](https://crates.io/crates/tonic-async-interceptor) 4 | [![Documentation](https://docs.rs/tonic-async-interceptor/badge.svg)](https://docs.rs/tonic-async-interceptor) 5 | [![Crates.io](https://img.shields.io/crates/l/tonic-async-interceptor)](LICENSE) 6 | 7 | This crate contains `AsyncInterceptor`, an async variant of Tonic's 8 | [`Interceptor`](https://docs.rs/tonic/latest/tonic/service/trait.Interceptor.html). 9 | Other than accepting an async interceptor function, it works the same as `Interceptor`. 10 | 11 | Async interceptor functions are useful for tasks like authentication, where you need to make 12 | asynchronous calls to another service or database within the interceptor. 13 | 14 | ## Compatibility 15 | 16 | The major/minor version corresponds with that of Tonic; so, if you are using Tonic v0.12.x, then you should likewise use Tonic Async Interceptor v0.12.x. 17 | 18 | ## Usage 19 | 20 | ### Using with Tonic built-in Server/Router 21 | 22 | ```rust 23 | async fn my_async_interceptor(req: Request<()>) -> Result, Status> { 24 | // do things and stuff 25 | Ok(req) 26 | } 27 | 28 | async fn main() { 29 | use tonic::transport::server; 30 | use tonic_async_interceptor::async_interceptor; 31 | let router = server::Server::builder() 32 | .layer(async_interceptor(my_async_interceptor)) 33 | .add_service(some_service) 34 | .add_service(another_service); 35 | // ... 36 | } 37 | ``` 38 | 39 | ### Setting a custom extension 40 | 41 | Here's an example of an async interceptor which authenticates a user and sets a custom 42 | extension for the underlying service to use. 43 | 44 | ```rust 45 | // Your custom extension 46 | struct UserContext { 47 | user_id: String, 48 | } 49 | 50 | // Async interceptor fn 51 | async fn authenticate(mut req: Request<()>) -> Result, Status> { 52 | // Inspect the gRPC metadata. 53 | let auth_header_val = match req.metadata().get("x-my-auth-header") { 54 | Some(val) => val, 55 | None => return Err(Status::unauthorized("Request missing creds")), 56 | }; 57 | // Call some async function (`verify_auth`). 58 | let maybe_user_id: Result = 59 | verify_auth(auth_header_val).await; 60 | 61 | let user_id = match maybe_user_id { 62 | Ok(id) => id, 63 | Err(_) => return Err(Status::unauthorized("Invalid creds")), 64 | }; 65 | 66 | // Insert an extension, which can be inspected by the service. 67 | req.extensions_mut().insert(UserContext { user_id }); 68 | 69 | Ok(req) 70 | } 71 | ``` 72 | 73 | ## Why is this a separate crate? 74 | 75 | The code in this crate was originally intended to live in the official Tonic crate, alongside 76 | the non-async interceptor code. However, the maintainer 77 | [decided not to merge it](https://github.com/hyperium/tonic/pull/910) 78 | due to lack of time and misalignment with his future vision for Tonic. 79 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! gRPC interceptors which are a kind of middleware. 2 | //! 3 | //! See [`Interceptor`] for more details. 4 | 5 | use bytes::Bytes; 6 | use http::{Extensions, Method, Uri, Version}; 7 | use http_body::Body; 8 | use pin_project::pin_project; 9 | use std::{ 10 | fmt, 11 | future::Future, 12 | mem, 13 | pin::Pin, 14 | task::{Context, Poll}, 15 | }; 16 | use tonic::body::Body as TonicBody; 17 | use tonic::metadata::MetadataMap; 18 | use tonic::{Request, Status}; 19 | use tower_layer::Layer; 20 | use tower_service::Service; 21 | 22 | pub type Error = Box; 23 | 24 | /// An async gRPC interceptor. 25 | /// 26 | /// This interceptor is an `async` variant of Tonic's built-in [`Interceptor`]. 27 | /// 28 | /// gRPC interceptors are similar to middleware but have less flexibility. An interceptor allows 29 | /// you to do two main things, one is to add/remove/check items in the `MetadataMap` of each 30 | /// request. Two, cancel a request with a `Status`. 31 | /// 32 | /// Any function that satisfies the bound `async FnMut(Request<()>) -> Result, Status>` 33 | /// can be used as an `AsyncInterceptor`. 34 | /// 35 | /// An interceptor can be used on both the server and client side through the `tonic-build` crate's 36 | /// generated structs. 37 | /// 38 | /// See the [interceptor example][example] for more details. 39 | /// 40 | /// If you need more powerful middleware, [tower] is the recommended approach. You can find 41 | /// examples of how to use tower with tonic [here][tower-example]. 42 | /// 43 | /// Additionally, interceptors is not the recommended way to add logging to your service. For that 44 | /// a [tower] middleware is more appropriate since it can also act on the response. For example 45 | /// tower-http's [`Trace`](https://docs.rs/tower-http/latest/tower_http/trace/index.html) 46 | /// middleware supports gRPC out of the box. 47 | /// 48 | /// [tower]: https://crates.io/crates/tower 49 | /// [example]: https://github.com/hyperium/tonic/tree/master/examples/src/interceptor 50 | /// [tower-example]: https://github.com/hyperium/tonic/tree/master/examples/src/tower 51 | /// 52 | /// Async version of `Interceptor`. 53 | pub trait AsyncInterceptor { 54 | /// The Future returned by the interceptor. 55 | type Future: Future, Status>>; 56 | /// Intercept a request before it is sent, optionally cancelling it. 57 | fn call(&mut self, request: Request<()>) -> Self::Future; 58 | } 59 | 60 | impl AsyncInterceptor for F 61 | where 62 | F: FnMut(Request<()>) -> U, 63 | U: Future, Status>>, 64 | { 65 | type Future = U; 66 | 67 | fn call(&mut self, request: Request<()>) -> Self::Future { 68 | self(request) 69 | } 70 | } 71 | 72 | /// Create a new async interceptor layer. 73 | /// 74 | /// See [`AsyncInterceptor`] and [`Interceptor`] for more details. 75 | pub fn async_interceptor(f: F) -> AsyncInterceptorLayer 76 | where 77 | F: AsyncInterceptor, 78 | { 79 | AsyncInterceptorLayer { f } 80 | } 81 | 82 | /// A gRPC async interceptor that can be used as a [`Layer`], 83 | /// created by calling [`async_interceptor`]. 84 | /// 85 | /// See [`AsyncInterceptor`] for more details. 86 | #[derive(Debug, Clone, Copy)] 87 | pub struct AsyncInterceptorLayer { 88 | f: F, 89 | } 90 | 91 | impl Layer for AsyncInterceptorLayer 92 | where 93 | S: Clone, 94 | F: AsyncInterceptor + Clone, 95 | { 96 | type Service = AsyncInterceptedService; 97 | 98 | fn layer(&self, service: S) -> Self::Service { 99 | AsyncInterceptedService::new(service, self.f.clone()) 100 | } 101 | } 102 | 103 | // Components and attributes of a request, without metadata or extensions. 104 | #[derive(Debug)] 105 | struct DecomposedRequest { 106 | uri: Uri, 107 | method: Method, 108 | http_version: Version, 109 | msg: ReqBody, 110 | } 111 | 112 | // Note that tonic::Request::into_parts is not public, so we do it this way. 113 | fn request_into_parts(mut req: Request) -> (MetadataMap, Extensions, Msg) { 114 | // We use mem::take because Tonic doesn't not provide public access to these fields. 115 | let metadata = mem::take(req.metadata_mut()); 116 | let extensions = mem::take(req.extensions_mut()); 117 | (metadata, extensions, req.into_inner()) 118 | } 119 | 120 | // Note that tonic::Request::from_parts is not public, so we do it this way. 121 | fn request_from_parts( 122 | msg: Msg, 123 | metadata: MetadataMap, 124 | extensions: Extensions, 125 | ) -> Request { 126 | let mut req = Request::new(msg); 127 | *req.metadata_mut() = metadata; 128 | *req.extensions_mut() = extensions; 129 | req 130 | } 131 | 132 | // Note that tonic::Request::into_http is not public, so we do it this way. 133 | fn request_into_http( 134 | msg: Msg, 135 | uri: http::Uri, 136 | method: http::Method, 137 | version: http::Version, 138 | metadata: MetadataMap, 139 | extensions: Extensions, 140 | ) -> http::Request { 141 | let mut request = http::Request::new(msg); 142 | *request.version_mut() = version; 143 | *request.method_mut() = method; 144 | *request.uri_mut() = uri; 145 | *request.headers_mut() = metadata.into_headers(); 146 | *request.extensions_mut() = extensions; 147 | 148 | request 149 | } 150 | 151 | /// Decompose the request into its contents and properties, and create a new request without a body. 152 | /// 153 | /// It is bad practice to modify the body (i.e. Message) of the request via an interceptor. 154 | /// To avoid exposing the body of the request to the interceptor function, we first remove it 155 | /// here, allow the interceptor to modify the metadata and extensions, and then recreate the 156 | /// HTTP request with the original message body with the `recompose` function. Also note that Tonic 157 | /// requests do not preserve the URI, HTTP version, and HTTP method of the HTTP request, so we 158 | /// extract them here and then add them back in `recompose`. 159 | fn decompose(req: http::Request) -> (DecomposedRequest, Request<()>) { 160 | let uri = req.uri().clone(); 161 | let method = req.method().clone(); 162 | let http_version = req.version(); 163 | let req = Request::from_http(req); 164 | let (metadata, extensions, msg) = request_into_parts(req); 165 | 166 | let dreq = DecomposedRequest { 167 | uri, 168 | method, 169 | http_version, 170 | msg, 171 | }; 172 | let req_without_body = request_from_parts((), metadata, extensions); 173 | 174 | (dreq, req_without_body) 175 | } 176 | 177 | /// Combine the modified metadata and extensions with the original message body and attributes. 178 | fn recompose( 179 | dreq: DecomposedRequest, 180 | modified_req: Request<()>, 181 | ) -> http::Request { 182 | let (metadata, extensions, _) = request_into_parts(modified_req); 183 | 184 | request_into_http( 185 | dreq.msg, 186 | dreq.uri, 187 | dreq.method, 188 | dreq.http_version, 189 | metadata, 190 | extensions, 191 | ) 192 | } 193 | 194 | /// A service wrapped in an async interceptor middleware. 195 | /// 196 | /// See [`AsyncInterceptor`] for more details. 197 | #[derive(Clone, Copy)] 198 | pub struct AsyncInterceptedService { 199 | inner: S, 200 | f: F, 201 | } 202 | 203 | impl AsyncInterceptedService { 204 | /// Create a new `AsyncInterceptedService` that wraps `S` and intercepts each request with the 205 | /// function `F`. 206 | pub fn new(service: S, f: F) -> Self { 207 | Self { inner: service, f } 208 | } 209 | } 210 | 211 | impl fmt::Debug for AsyncInterceptedService 212 | where 213 | S: fmt::Debug, 214 | { 215 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 216 | f.debug_struct("AsyncInterceptedService") 217 | .field("inner", &self.inner) 218 | .field("f", &format_args!("{}", std::any::type_name::())) 219 | .finish() 220 | } 221 | } 222 | 223 | impl Service> for AsyncInterceptedService 224 | where 225 | F: AsyncInterceptor + Clone, 226 | S: Service, Response = http::Response> + Clone, 227 | S::Error: Into, 228 | ReqBody: Default, 229 | ResBody: Default + Body + Send + 'static, 230 | ResBody::Error: Into, 231 | { 232 | type Response = http::Response; 233 | type Error = S::Error; 234 | type Future = AsyncResponseFuture; 235 | 236 | #[inline] 237 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 238 | self.inner.poll_ready(cx) 239 | } 240 | 241 | fn call(&mut self, req: http::Request) -> Self::Future { 242 | // This is necessary because tonic internally uses `tower::buffer::Buffer`. 243 | // See https://github.com/tower-rs/tower/issues/547#issuecomment-767629149 244 | // for details on why this is necessary 245 | let clone = self.inner.clone(); 246 | let inner = std::mem::replace(&mut self.inner, clone); 247 | 248 | AsyncResponseFuture::new(req, &mut self.f, inner) 249 | } 250 | } 251 | 252 | // required to use `AsyncInterceptedService` with `Router` 253 | impl tonic::server::NamedService for AsyncInterceptedService 254 | where 255 | S: tonic::server::NamedService, 256 | { 257 | const NAME: &'static str = S::NAME; 258 | } 259 | 260 | /// Response future for [`InterceptedService`]. 261 | #[pin_project] 262 | #[derive(Debug)] 263 | pub struct ResponseFuture { 264 | #[pin] 265 | kind: Kind, 266 | } 267 | 268 | impl ResponseFuture { 269 | fn future(future: F) -> Self { 270 | Self { 271 | kind: Kind::Future(future), 272 | } 273 | } 274 | 275 | fn status(status: Status) -> Self { 276 | Self { 277 | kind: Kind::Status(Some(status)), 278 | } 279 | } 280 | } 281 | 282 | #[pin_project(project = KindProj)] 283 | #[derive(Debug)] 284 | enum Kind { 285 | Future(#[pin] F), 286 | Status(Option), 287 | } 288 | 289 | impl Future for ResponseFuture 290 | where 291 | F: Future, E>>, 292 | E: Into, 293 | B: Default + Body + Send + 'static, 294 | B::Error: Into, 295 | { 296 | type Output = Result, E>; 297 | 298 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 299 | match self.project().kind.project() { 300 | KindProj::Future(future) => future 301 | .poll(cx) 302 | .map(|result| result.map(|resp| resp.map(TonicBody::new))), 303 | KindProj::Status(status) => { 304 | let response = status.take().unwrap().into_http(); 305 | Poll::Ready(Ok(response)) 306 | } 307 | } 308 | } 309 | } 310 | 311 | #[pin_project(project = PinnedOptionProj)] 312 | #[derive(Debug)] 313 | enum PinnedOption { 314 | Some(#[pin] F), 315 | None, 316 | } 317 | 318 | /// Response future for [`AsyncInterceptedService`]. 319 | /// 320 | /// Handles the call to the async interceptor, then calls the inner service and wraps the result in 321 | /// [`ResponseFuture`]. 322 | #[pin_project(project = AsyncResponseFutureProj)] 323 | #[derive(Debug)] 324 | pub struct AsyncResponseFuture 325 | where 326 | S: Service>, 327 | S::Error: Into, 328 | I: Future, Status>>, 329 | { 330 | #[pin] 331 | interceptor_fut: PinnedOption, 332 | #[pin] 333 | inner_fut: PinnedOption>, 334 | inner: S, 335 | dreq: DecomposedRequest, 336 | } 337 | 338 | impl AsyncResponseFuture 339 | where 340 | S: Service>, 341 | S::Error: Into, 342 | I: Future, Status>>, 343 | ReqBody: Default, 344 | { 345 | fn new>( 346 | req: http::Request, 347 | interceptor: &mut A, 348 | inner: S, 349 | ) -> Self { 350 | let (dreq, req_without_body) = decompose(req); 351 | let interceptor_fut = interceptor.call(req_without_body); 352 | 353 | AsyncResponseFuture { 354 | interceptor_fut: PinnedOption::Some(interceptor_fut), 355 | inner_fut: PinnedOption::None, 356 | inner, 357 | dreq, 358 | } 359 | } 360 | 361 | /// Calls the inner service with the intercepted request (which has been modified by the 362 | /// async interceptor func). 363 | fn create_inner_fut( 364 | this: &mut AsyncResponseFutureProj<'_, S, I, ReqBody>, 365 | intercepted_req: Result, Status>, 366 | ) -> ResponseFuture { 367 | match intercepted_req { 368 | Ok(req) => { 369 | // We can't move the message body out of the pin projection. So, to 370 | // avoid copying it, we swap its memory with an empty body and then can 371 | // move it into the recomposed request. 372 | let msg = mem::take(&mut this.dreq.msg); 373 | let movable_dreq = DecomposedRequest { 374 | uri: this.dreq.uri.clone(), 375 | method: this.dreq.method.clone(), 376 | http_version: this.dreq.http_version, 377 | msg, 378 | }; 379 | let modified_req_with_body = recompose(movable_dreq, req); 380 | 381 | ResponseFuture::future(this.inner.call(modified_req_with_body)) 382 | } 383 | Err(status) => ResponseFuture::status(status), 384 | } 385 | } 386 | } 387 | 388 | impl Future for AsyncResponseFuture 389 | where 390 | S: Service, Response = http::Response>, 391 | I: Future, Status>>, 392 | S::Error: Into, 393 | ReqBody: Default, 394 | ResBody: Default + Body + Send + 'static, 395 | ResBody::Error: Into, 396 | { 397 | type Output = Result, S::Error>; 398 | 399 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 400 | let mut this = self.project(); 401 | 402 | // The struct was initialized (via `new`) with interceptor func future, which we poll here. 403 | if let PinnedOptionProj::Some(f) = this.interceptor_fut.as_mut().project() { 404 | match f.poll(cx) { 405 | Poll::Ready(intercepted_req) => { 406 | let inner_fut = AsyncResponseFuture::::create_inner_fut( 407 | &mut this, 408 | intercepted_req, 409 | ); 410 | // Set the inner service future and clear the interceptor future. 411 | this.inner_fut.set(PinnedOption::Some(inner_fut)); 412 | this.interceptor_fut.set(PinnedOption::None); 413 | } 414 | Poll::Pending => return Poll::Pending, 415 | } 416 | } 417 | // At this point, inner_fut should always be Some. 418 | let inner_fut = match this.inner_fut.project() { 419 | PinnedOptionProj::None => panic!(), 420 | PinnedOptionProj::Some(f) => f, 421 | }; 422 | 423 | inner_fut.poll(cx) 424 | } 425 | } 426 | 427 | #[cfg(test)] 428 | mod tests { 429 | use super::*; 430 | use http::StatusCode; 431 | use http_body_util::Empty; 432 | use std::future; 433 | use tower::ServiceExt; 434 | 435 | #[tokio::test] 436 | async fn propagates_added_extensions() { 437 | #[derive(Clone)] 438 | struct TestExtension { 439 | data: String, 440 | } 441 | let test_extension_data = "abc"; 442 | 443 | let layer = async_interceptor(|mut req: Request<()>| { 444 | req.extensions_mut().insert(TestExtension { 445 | data: test_extension_data.to_owned(), 446 | }); 447 | 448 | future::ready(Ok(req)) 449 | }); 450 | 451 | let svc = layer.layer(tower::service_fn( 452 | |http_req: http::Request>| async { 453 | let req = Request::from_http(http_req); 454 | let maybe_extension = req.extensions().get::(); 455 | assert!(maybe_extension.is_some()); 456 | assert_eq!(maybe_extension.unwrap().data, test_extension_data); 457 | 458 | Ok::<_, Status>(http::Response::new(Empty::new())) 459 | }, 460 | )); 461 | 462 | let request = http::Request::builder().body(Empty::new()).unwrap(); 463 | let http_response = svc.oneshot(request).await.unwrap(); 464 | 465 | assert_eq!(http_response.status(), StatusCode::OK); 466 | } 467 | 468 | #[tokio::test] 469 | async fn propagates_added_metadata() { 470 | let test_metadata_key = "test_key"; 471 | let test_metadata_val = "abc"; 472 | 473 | let layer = async_interceptor(|mut req: Request<()>| { 474 | req.metadata_mut() 475 | .insert(test_metadata_key, test_metadata_val.parse().unwrap()); 476 | 477 | future::ready(Ok(req)) 478 | }); 479 | 480 | let svc = layer.layer(tower::service_fn( 481 | |http_req: http::Request>| async { 482 | let req = Request::from_http(http_req); 483 | let maybe_metadata = req.metadata().get(test_metadata_key); 484 | assert!(maybe_metadata.is_some()); 485 | assert_eq!(maybe_metadata.unwrap(), test_metadata_val); 486 | 487 | Ok::<_, Status>(http::Response::new(Empty::new())) 488 | }, 489 | )); 490 | 491 | let request = http::Request::builder().body(Empty::new()).unwrap(); 492 | let http_response = svc.oneshot(request).await.unwrap(); 493 | 494 | assert_eq!(http_response.status(), StatusCode::OK); 495 | } 496 | 497 | #[tokio::test] 498 | async fn doesnt_remove_headers_from_request() { 499 | let layer = async_interceptor(|request: Request<()>| { 500 | assert_eq!( 501 | request 502 | .metadata() 503 | .get("user-agent") 504 | .expect("missing in interceptor"), 505 | "test-tonic" 506 | ); 507 | future::ready(Ok(request)) 508 | }); 509 | 510 | let svc = layer.layer(tower::service_fn( 511 | |request: http::Request>| async move { 512 | assert_eq!( 513 | request 514 | .headers() 515 | .get("user-agent") 516 | .expect("missing in leaf service"), 517 | "test-tonic" 518 | ); 519 | 520 | Ok::<_, Status>(http::Response::new(Empty::new())) 521 | }, 522 | )); 523 | 524 | let request = http::Request::builder() 525 | .header("user-agent", "test-tonic") 526 | .body(Empty::new()) 527 | .unwrap(); 528 | 529 | svc.oneshot(request).await.unwrap(); 530 | } 531 | 532 | #[tokio::test] 533 | async fn handles_intercepted_status_as_response() { 534 | let message = "Blocked by the interceptor"; 535 | let expected = Status::permission_denied(message).into_http::(); 536 | 537 | let layer = async_interceptor(|_: Request<()>| { 538 | future::ready(Err(Status::permission_denied(message))) 539 | }); 540 | 541 | let svc = layer.layer(tower::service_fn(|_: http::Request>| async { 542 | Ok::<_, Status>(http::Response::new(Empty::new())) 543 | })); 544 | 545 | let request = http::Request::builder().body(Empty::new()).unwrap(); 546 | let response = svc.oneshot(request).await.unwrap(); 547 | 548 | assert_eq!(expected.status(), response.status()); 549 | assert_eq!(expected.version(), response.version()); 550 | assert_eq!(expected.headers(), response.headers()); 551 | } 552 | 553 | #[tokio::test] 554 | async fn doesnt_change_http_method() { 555 | let layer = async_interceptor(|request: Request<()>| future::ready(Ok(request))); 556 | 557 | let svc = layer.layer(tower::service_fn( 558 | |request: http::Request>| async move { 559 | assert_eq!(request.method(), http::Method::OPTIONS); 560 | 561 | Ok::<_, Status>(http::Response::new(Empty::new())) 562 | }, 563 | )); 564 | 565 | let request = http::Request::builder() 566 | .method(http::Method::OPTIONS) 567 | .body(Empty::new()) 568 | .unwrap(); 569 | 570 | svc.oneshot(request).await.unwrap(); 571 | } 572 | } 573 | --------------------------------------------------------------------------------