Skip to main content

tonic_web/
service.rs

1/*
2 *
3 * Copyright 2025 gRPC authors.
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
7 * deal in the Software without restriction, including without limitation the
8 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9 * sell 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
13 * all 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
20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21 * IN THE SOFTWARE.
22 *
23 */
24
25use core::fmt;
26use std::future::Future;
27use std::pin::Pin;
28use std::task::{Context, Poll, ready};
29
30use http::{HeaderMap, HeaderValue, Method, Request, Response, StatusCode, Version, header};
31use pin_project::pin_project;
32use tonic::metadata::GRPC_CONTENT_TYPE;
33use tonic::{body::Body, server::NamedService};
34use tower_service::Service;
35use tracing::{debug, trace};
36
37use crate::call::content_types::is_grpc_web;
38use crate::call::{Encoding, GrpcWebCall};
39
40/// Service implementing the grpc-web protocol.
41#[derive(Debug, Clone)]
42pub struct GrpcWebService<S> {
43    inner: S,
44}
45
46#[derive(Debug, PartialEq)]
47enum RequestKind<'a> {
48    // The request is considered a grpc-web request if its `content-type`
49    // header is exactly one of:
50    //
51    //  - "application/grpc-web"
52    //  - "application/grpc-web+proto"
53    //  - "application/grpc-web-text"
54    //  - "application/grpc-web-text+proto"
55    GrpcWeb {
56        method: &'a Method,
57        encoding: Encoding,
58        accept: Encoding,
59    },
60    // All other requests, including `application/grpc`
61    Other(http::Version),
62}
63
64impl<S> GrpcWebService<S> {
65    pub(crate) fn new(inner: S) -> Self {
66        GrpcWebService { inner }
67    }
68}
69
70impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for GrpcWebService<S>
71where
72    S: Service<Request<Body>, Response = Response<ResBody>>,
73    ReqBody: http_body::Body<Data = bytes::Bytes> + Send + 'static,
74    ReqBody::Error: Into<crate::BoxError> + fmt::Display,
75    ResBody: http_body::Body<Data = bytes::Bytes> + Send + 'static,
76    ResBody::Error: Into<crate::BoxError> + fmt::Display,
77{
78    type Response = Response<Body>;
79    type Error = S::Error;
80    type Future = ResponseFuture<S::Future>;
81
82    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
83        self.inner.poll_ready(cx)
84    }
85
86    fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
87        match RequestKind::new(req.headers(), req.method(), req.version()) {
88            // A valid grpc-web request, regardless of HTTP version.
89            //
90            // If the request includes an `origin` header, we verify it is allowed
91            // to access the resource, an HTTP 403 response is returned otherwise.
92            //
93            // If the origin is allowed to access the resource or there is no
94            // `origin` header present, translate the request into a grpc request,
95            // call the inner service, and translate the response back to
96            // grpc-web.
97            RequestKind::GrpcWeb {
98                method: &Method::POST,
99                encoding,
100                accept,
101            } => {
102                trace!(kind = "simple", path = ?req.uri().path(), ?encoding, ?accept);
103
104                ResponseFuture {
105                    case: Case::GrpcWeb {
106                        future: self.inner.call(coerce_request(req, encoding)),
107                        accept,
108                    },
109                }
110            }
111
112            // The request's content-type matches one of the 4 supported grpc-web
113            // content-types, but the request method is not `POST`.
114            // This is not a valid grpc-web request, return HTTP 405.
115            RequestKind::GrpcWeb { .. } => {
116                debug!(kind = "simple", error="method not allowed", method = ?req.method());
117
118                ResponseFuture {
119                    case: Case::immediate(StatusCode::METHOD_NOT_ALLOWED),
120                }
121            }
122
123            // All http/2 requests that are not grpc-web are passed through to the inner service,
124            // whatever they are.
125            RequestKind::Other(Version::HTTP_2) => {
126                debug!(kind = "other h2", content_type = ?req.headers().get(header::CONTENT_TYPE));
127                ResponseFuture {
128                    case: Case::Other {
129                        future: self.inner.call(req.map(Body::new)),
130                    },
131                }
132            }
133
134            // Return HTTP 400 for all other requests.
135            RequestKind::Other(_) => {
136                debug!(kind = "other h1", content_type = ?req.headers().get(header::CONTENT_TYPE));
137
138                ResponseFuture {
139                    case: Case::immediate(StatusCode::BAD_REQUEST),
140                }
141            }
142        }
143    }
144}
145
146/// Response future for the [`GrpcWebService`].
147#[pin_project]
148#[must_use = "futures do nothing unless polled"]
149pub struct ResponseFuture<F> {
150    #[pin]
151    case: Case<F>,
152}
153
154#[pin_project(project = CaseProj)]
155enum Case<F> {
156    GrpcWeb {
157        #[pin]
158        future: F,
159        accept: Encoding,
160    },
161    Other {
162        #[pin]
163        future: F,
164    },
165    ImmediateResponse {
166        res: Option<http::response::Parts>,
167    },
168}
169
170impl<F> Case<F> {
171    fn immediate(status: StatusCode) -> Self {
172        let (res, ()) = Response::builder()
173            .status(status)
174            .body(())
175            .unwrap()
176            .into_parts();
177        Self::ImmediateResponse { res: Some(res) }
178    }
179}
180
181impl<F, B, E> Future for ResponseFuture<F>
182where
183    F: Future<Output = Result<Response<B>, E>>,
184    B: http_body::Body<Data = bytes::Bytes> + Send + 'static,
185    B::Error: Into<crate::BoxError> + fmt::Display,
186{
187    type Output = Result<Response<Body>, E>;
188
189    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
190        let this = self.project();
191
192        match this.case.project() {
193            CaseProj::GrpcWeb { future, accept } => {
194                let res = ready!(future.poll(cx))?;
195
196                Poll::Ready(Ok(coerce_response(res, *accept)))
197            }
198            CaseProj::Other { future } => future.poll(cx).map_ok(|res| res.map(Body::new)),
199            CaseProj::ImmediateResponse { res } => {
200                let res = Response::from_parts(res.take().unwrap(), Body::empty());
201                Poll::Ready(Ok(res))
202            }
203        }
204    }
205}
206
207impl<S: NamedService> NamedService for GrpcWebService<S> {
208    const NAME: &'static str = S::NAME;
209}
210
211impl<F> fmt::Debug for ResponseFuture<F> {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        f.debug_struct("ResponseFuture").finish()
214    }
215}
216
217impl<'a> RequestKind<'a> {
218    fn new(headers: &'a HeaderMap, method: &'a Method, version: Version) -> Self {
219        if is_grpc_web(headers) {
220            return RequestKind::GrpcWeb {
221                method,
222                encoding: Encoding::from_content_type(headers),
223                accept: Encoding::from_accept(headers),
224            };
225        }
226
227        RequestKind::Other(version)
228    }
229}
230
231// Mutating request headers to conform to a gRPC request is not really
232// necessary for us at this point. We could remove most of these except
233// maybe for inserting `header::TE`, which tonic should check?
234fn coerce_request<B>(mut req: Request<B>, encoding: Encoding) -> Request<Body>
235where
236    B: http_body::Body<Data = bytes::Bytes> + Send + 'static,
237    B::Error: Into<crate::BoxError> + fmt::Display,
238{
239    req.headers_mut().remove(header::CONTENT_LENGTH);
240
241    req.headers_mut()
242        .insert(header::CONTENT_TYPE, GRPC_CONTENT_TYPE);
243
244    req.headers_mut()
245        .insert(header::TE, HeaderValue::from_static("trailers"));
246
247    req.headers_mut().insert(
248        header::ACCEPT_ENCODING,
249        HeaderValue::from_static("identity,deflate,gzip"),
250    );
251
252    req.map(|b| Body::new(GrpcWebCall::request(b, encoding)))
253}
254
255fn coerce_response<B>(res: Response<B>, encoding: Encoding) -> Response<Body>
256where
257    B: http_body::Body<Data = bytes::Bytes> + Send + 'static,
258    B::Error: Into<crate::BoxError> + fmt::Display,
259{
260    let mut res = res
261        .map(|b| GrpcWebCall::response(b, encoding))
262        .map(Body::new);
263
264    res.headers_mut().insert(
265        header::CONTENT_TYPE,
266        HeaderValue::from_static(encoding.to_content_type()),
267    );
268
269    res
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use crate::call::content_types::*;
276    use http::header::{
277        ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, CONTENT_TYPE, ORIGIN,
278    };
279    use tower_layer::Layer as _;
280
281    type BoxFuture<T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send>>;
282
283    #[derive(Debug, Clone)]
284    struct Svc;
285
286    impl<B> tower_service::Service<Request<B>> for Svc {
287        type Response = Response<Body>;
288        type Error = std::convert::Infallible;
289        type Future = BoxFuture<Self::Response, Self::Error>;
290
291        fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
292            Poll::Ready(Ok(()))
293        }
294
295        fn call(&mut self, _: Request<B>) -> Self::Future {
296            Box::pin(async { Ok(Response::new(Body::default())) })
297        }
298    }
299
300    impl NamedService for Svc {
301        const NAME: &'static str = "test";
302    }
303
304    fn enable<S>(service: S) -> tower_http::cors::Cors<GrpcWebService<S>>
305    where
306        S: Service<http::Request<Body>, Response = http::Response<Body>>,
307    {
308        tower_layer::Stack::new(
309            crate::GrpcWebLayer::new(),
310            tower_http::cors::CorsLayer::new(),
311        )
312        .layer(service)
313    }
314
315    mod grpc_web {
316        use super::*;
317        use tower_layer::Layer;
318
319        fn request() -> Request<Body> {
320            Request::builder()
321                .method(Method::POST)
322                .header(CONTENT_TYPE, GRPC_WEB)
323                .header(ORIGIN, "http://example.com")
324                .body(Body::default())
325                .unwrap()
326        }
327
328        #[tokio::test]
329        async fn default_cors_config() {
330            let mut svc = enable(Svc);
331            let res = svc.call(request()).await.unwrap();
332
333            assert_eq!(res.status(), StatusCode::OK);
334        }
335
336        #[tokio::test]
337        async fn web_layer() {
338            let mut svc = crate::GrpcWebLayer::new().layer(Svc);
339            let res = svc.call(request()).await.unwrap();
340
341            assert_eq!(res.status(), StatusCode::OK);
342        }
343
344        #[tokio::test]
345        async fn web_layer_with_axum() {
346            let mut svc = axum::routing::Router::new()
347                .route("/", axum::routing::post_service(Svc))
348                .layer(crate::GrpcWebLayer::new());
349
350            let res = svc.call(request()).await.unwrap();
351
352            assert_eq!(res.status(), StatusCode::OK);
353        }
354
355        #[tokio::test]
356        async fn without_origin() {
357            let mut svc = enable(Svc);
358
359            let mut req = request();
360            req.headers_mut().remove(ORIGIN);
361
362            let res = svc.call(req).await.unwrap();
363
364            assert_eq!(res.status(), StatusCode::OK);
365        }
366
367        #[tokio::test]
368        async fn only_post_and_options_allowed() {
369            let mut svc = enable(Svc);
370
371            for method in &[
372                Method::GET,
373                Method::PUT,
374                Method::DELETE,
375                Method::HEAD,
376                Method::PATCH,
377            ] {
378                let mut req = request();
379                *req.method_mut() = method.clone();
380
381                let res = svc.call(req).await.unwrap();
382
383                assert_eq!(
384                    res.status(),
385                    StatusCode::METHOD_NOT_ALLOWED,
386                    "{method} should not be allowed"
387                );
388            }
389        }
390
391        #[tokio::test]
392        async fn grpc_web_content_types() {
393            let mut svc = enable(Svc);
394
395            for ct in &[GRPC_WEB_TEXT, GRPC_WEB_PROTO, GRPC_WEB_TEXT_PROTO, GRPC_WEB] {
396                let mut req = request();
397                req.headers_mut()
398                    .insert(CONTENT_TYPE, HeaderValue::from_static(ct));
399
400                let res = svc.call(req).await.unwrap();
401
402                assert_eq!(res.status(), StatusCode::OK);
403            }
404        }
405    }
406
407    mod options {
408        use super::*;
409
410        fn request() -> Request<Body> {
411            Request::builder()
412                .method(Method::OPTIONS)
413                .header(ORIGIN, "http://example.com")
414                .header(ACCESS_CONTROL_REQUEST_HEADERS, "x-grpc-web")
415                .header(ACCESS_CONTROL_REQUEST_METHOD, "POST")
416                .body(Body::default())
417                .unwrap()
418        }
419
420        #[tokio::test]
421        async fn valid_grpc_web_preflight() {
422            let mut svc = enable(Svc);
423            let res = svc.call(request()).await.unwrap();
424
425            assert_eq!(res.status(), StatusCode::OK);
426        }
427    }
428
429    mod grpc {
430        use super::*;
431
432        fn request() -> Request<Body> {
433            Request::builder()
434                .version(Version::HTTP_2)
435                .header(CONTENT_TYPE, GRPC_CONTENT_TYPE)
436                .body(Body::default())
437                .unwrap()
438        }
439
440        #[tokio::test]
441        async fn h2_is_ok() {
442            let mut svc = enable(Svc);
443
444            let req = request();
445            let res = svc.call(req).await.unwrap();
446
447            assert_eq!(res.status(), StatusCode::OK)
448        }
449
450        #[tokio::test]
451        async fn h1_is_err() {
452            let mut svc = enable(Svc);
453
454            let req = Request::builder()
455                .header(CONTENT_TYPE, GRPC_CONTENT_TYPE)
456                .body(Body::default())
457                .unwrap();
458
459            let res = svc.call(req).await.unwrap();
460            assert_eq!(res.status(), StatusCode::BAD_REQUEST)
461        }
462
463        #[tokio::test]
464        async fn content_type_variants() {
465            let mut svc = enable(Svc);
466
467            for variant in &["grpc", "grpc+proto", "grpc+thrift", "grpc+foo"] {
468                let mut req = request();
469                req.headers_mut().insert(
470                    CONTENT_TYPE,
471                    HeaderValue::from_maybe_shared(format!("application/{variant}")).unwrap(),
472                );
473
474                let res = svc.call(req).await.unwrap();
475
476                assert_eq!(res.status(), StatusCode::OK)
477            }
478        }
479    }
480
481    mod other {
482        use super::*;
483
484        fn request() -> Request<Body> {
485            Request::builder()
486                .header(CONTENT_TYPE, "application/text")
487                .body(Body::default())
488                .unwrap()
489        }
490
491        #[tokio::test]
492        async fn h1_is_err() {
493            let mut svc = enable(Svc);
494            let res = svc.call(request()).await.unwrap();
495
496            assert_eq!(res.status(), StatusCode::BAD_REQUEST)
497        }
498
499        #[tokio::test]
500        async fn h2_is_ok() {
501            let mut svc = enable(Svc);
502            let mut req = request();
503            *req.version_mut() = Version::HTTP_2;
504
505            let res = svc.call(req).await.unwrap();
506            assert_eq!(res.status(), StatusCode::OK)
507        }
508    }
509}