Skip to main content

ntex_grpc/client/
mod.rs

1#![allow(async_fn_in_trait)]
2
3use std::{borrow::Cow, fmt};
4
5use ntex_bytes::{ByteString, Bytes};
6use ntex_error::ErrorDiagnostic;
7use ntex_h2::{OperationError, StreamError, client};
8use ntex_http::{HeaderMap, StatusCode, error::Error as HttpError, header};
9
10pub(crate) mod request;
11mod stream;
12pub(crate) mod transport;
13
14pub use self::request::{Request, RequestContext, Response};
15pub use self::stream::{
16    Bidi, ClientStreaming, ServerStreaming, StreamCall, StreamRequest, StreamTransport,
17};
18
19use crate::{consts, encoding::DecodeError, service::MethodDef, status::GrpcStatus, utils};
20
21/// Sends unary calls of method `T`.
22///
23/// Generated clients go through this trait for every call. Implement it to
24/// put something between the client and the connection, like metrics or
25/// retries. [`Client`], [`ntex_h2::client::Client`] and
26/// [`ntex_h2::client::SimpleClient`] implement it.
27pub trait Transport<T: MethodDef, St = ()> {
28    /// Errors produced by the transport.
29    type Error;
30
31    /// Send `args` as the request message and wait for the reply.
32    ///
33    /// `ctx` holds the headers and timeout set on the [`Request`]. If
34    /// [`RequestContext::take_error()`] returns an error, return it without
35    /// sending anything.
36    async fn request(
37        &self,
38        args: &T::Input,
39        ctx: &mut RequestContext<St>,
40    ) -> Result<Response<T>, Self::Error>;
41}
42
43/// Client utils methods
44pub trait ClientInformation<T> {
45    /// Create new client instance
46    fn create(transport: T) -> Self;
47
48    /// Get reference to underlying transport
49    fn transport(&self) -> &T;
50
51    /// Get mut reference to underlying transport
52    fn transport_mut(&mut self) -> &mut T;
53
54    /// Consume client and return inner transport
55    fn into_inner(self) -> T;
56}
57
58/// [`Transport`] on top of an `ntex-h2` connection pool.
59///
60/// Cloning is cheap and clones share the pool, copying the message-size
61/// defaults. [`max_message_size()`](Self::max_message_size) and
62/// [`max_send_message_size()`](Self::max_send_message_size) set defaults for
63/// unary and streaming calls; explicit per-request limits take precedence.
64/// The pool itself, [`ntex_h2::client::Client`], is a transport as well, but
65/// does not carry these gRPC defaults.
66#[derive(Clone)]
67pub struct Client {
68    pool: client::Client,
69    max_message_size: usize,
70    max_send_message_size: usize,
71}
72
73impl Client {
74    #[inline]
75    /// Create grpc client transport from h2 client
76    pub fn new(client: client::Client) -> Self {
77        Self {
78            pool: client,
79            max_message_size: request::DEFAULT_MAX_MESSAGE_SIZE,
80            max_send_message_size: request::DEFAULT_MAX_SEND_MESSAGE_SIZE,
81        }
82    }
83
84    #[must_use]
85    /// Set the default size limit of each response message, in bytes.
86    ///
87    /// The default is 4 MiB. Both the received and decompressed message
88    /// must fit. A larger message fails the call with
89    /// [`GrpcStatus::ResourceExhausted`].
90    ///
91    /// [`Request::max_message_size()`] and
92    /// [`StreamRequest::max_message_size()`] override this for one call.
93    pub fn max_message_size(mut self, size: usize) -> Self {
94        self.max_message_size = size;
95        self
96    }
97
98    #[must_use]
99    /// Set the default size limit of each request message, in bytes.
100    ///
101    /// The default is 2 GiB - 1. The limit applies after compression. A
102    /// larger message is not sent and fails with
103    /// [`GrpcStatus::ResourceExhausted`]. Messages of 4 GiB or larger cannot
104    /// be sent, regardless of this limit.
105    ///
106    /// [`Request::max_send_message_size()`] and
107    /// [`StreamRequest::max_send_message_size()`] override this for one call.
108    pub fn max_send_message_size(mut self, size: usize) -> Self {
109        self.max_send_message_size = size;
110        self
111    }
112
113    #[inline]
114    /// Get reference to h2 client
115    pub fn get_ref(&self) -> &client::Client {
116        &self.pool
117    }
118}
119
120/// Why a call failed.
121///
122/// Built-in transports return it wrapped in [`ntex_error::Error`], which
123/// derefs to `ClientError`, so match on `&*err`.
124///
125/// Each variant calls for a different reaction from a caller that retries
126/// calls or tracks the health of endpoints. Within a variant,
127/// [`message()`](Self::message) describes the cause, and
128/// [`status()`](Self::status) gives the gRPC code of every variant.
129///
130/// | Variant | Server saw the request? | What a caller can do |
131/// | --- | --- | --- |
132/// | [`Status`](Self::Status) | yes | apply the rules for the code |
133/// | [`InvalidRequest`](Self::InvalidRequest) | no | fail the call, retrying fails the same way |
134/// | [`NotProcessed`](Self::NotProcessed) | no | retry at once, for any method |
135/// | [`Connection`](Self::Connection) | maybe, see `request_sent` | retry elsewhere |
136/// | [`StreamFailed`](Self::StreamFailed) | maybe | retry if the method is idempotent |
137/// | [`NotGrpc`](Self::NotGrpc) | not by a grpc server | check the address, or back off on 429, 502, 503 and 504 |
138/// | [`InvalidResponse`](Self::InvalidResponse) | yes | fail the call, asking again gets the same answer |
139/// | [`DeadlineExceeded`](Self::DeadlineExceeded) | maybe | the time for the call is up |
140///
141/// ```rust,ignore
142/// match req.send().await {
143///     // `req` comes from a generated client, e.g. `client.say_hello(&msg)`
144///     Ok(res) => println!("{}", res.message),
145///     Err(err) => match &*err {
146///         // the server replied, but not with OK
147///         ClientError::Status(st) => println!("{:?}: {}", st.code, err.message()),
148///         // safe to retry, the server did not process the request
149///         ClientError::NotProcessed(_) => println!("retry"),
150///         _ => println!("call failed with {:?}: {err}", err.status()),
151///     },
152/// }
153/// ```
154#[derive(Clone, Debug)]
155#[non_exhaustive]
156pub enum ClientError {
157    /// The server replied with a `grpc-status` other than `OK`.
158    ///
159    /// The only variant that holds a status the server sent, including
160    /// `DEADLINE_EXCEEDED`.
161    Status(Status),
162    /// The request is not valid, nothing was sent and sending it again fails
163    /// the same way.
164    ///
165    /// A header with an invalid name or value (see
166    /// [`RequestContext::take_error()`]), headers over the server's limit, a
167    /// message over [`Request::max_send_message_size()`] or a message that
168    /// does not compress.
169    InvalidRequest(CallFailure),
170    /// The server did not process the request, retrying is safe for any
171    /// method.
172    ///
173    /// The connection was closing or out of stream identifiers when the call
174    /// started, the server refused the stream with `REFUSED_STREAM`, or the
175    /// server's GOAWAY did not cover the stream.
176    NotProcessed(CallFailure),
177    /// The connection could not be established, or it failed or closed
178    /// before the call ended.
179    Connection {
180        /// Whether the request was handed to the connection.
181        ///
182        /// If false the server did not see the request and retrying is safe
183        /// for any method, otherwise the server may have processed it.
184        request_sent: bool,
185        /// The failure, its source is the `ntex-h2` error.
186        failure: CallFailure,
187    },
188    /// The stream of the call failed while the connection stayed up.
189    ///
190    /// The server reset it, or broke the protocol on it, or the client reset
191    /// it because it could not send. The server may have processed the
192    /// request. A reset is mapped to a code as the gRPC spec says, e.g.
193    /// `CANCEL` to `CANCELLED` and `ENHANCE_YOUR_CALM` to
194    /// `RESOURCE_EXHAUSTED`.
195    StreamFailed(CallFailure),
196    /// Something other than a grpc server replied: the HTTP status is not 200
197    /// or the `content-type` is not `application/grpc`.
198    NotGrpc(NotGrpc),
199    /// The server replied with a grpc response the client cannot accept.
200    ///
201    /// The response has an invalid or no `grpc-status`, ends without
202    /// trailers or a complete message, has data after the message, a message
203    /// over [`Request::max_message_size()`], in an unknown encoding or that
204    /// does not decode.
205    InvalidResponse(CallFailure),
206    /// The request timeout ran out before the server replied.
207    ///
208    /// The client stopped waiting, the stream is reset with `CANCEL` then,
209    /// or the server reset the stream with `CANCEL` once the timeout had run
210    /// out. A `DEADLINE_EXCEEDED` status from the server is a
211    /// [`Status`](Self::Status).
212    DeadlineExceeded(CallFailure),
213}
214
215/// A status other than `OK` that the server sent.
216#[derive(Clone, Debug)]
217#[non_exhaustive]
218pub struct Status {
219    /// The status code.
220    pub code: GrpcStatus,
221    /// The trailers, or the headers of a trailers-only response.
222    ///
223    /// They hold `grpc-message`, `grpc-status-details-bin` and the other
224    /// metadata from the server.
225    pub metadata: HeaderMap,
226}
227
228/// A failure the client detected.
229#[derive(Clone, Debug)]
230#[non_exhaustive]
231pub struct CallFailure {
232    /// The gRPC code of the failure, as the gRPC spec maps it.
233    ///
234    /// E.g. `UNAVAILABLE` for a lost connection or a refused stream,
235    /// `RESOURCE_EXHAUSTED` for a message over a limit.
236    pub code: GrpcStatus,
237    /// What happened, e.g. "Connection is disconnecting" or "Response ended
238    /// without trailers".
239    pub message: ByteString,
240    /// The response headers received before the failure, or the trailers
241    /// if the response ended. Empty if none arrived.
242    pub headers: HeaderMap,
243    /// The error behind the failure, also returned by
244    /// [`source()`](std::error::Error::source).
245    pub cause: Option<Cause>,
246}
247
248/// A response from something other than a grpc server.
249#[derive(Clone, Debug)]
250#[non_exhaustive]
251pub struct NotGrpc {
252    /// The HTTP status of the response, if it had one.
253    pub http_status: Option<StatusCode>,
254    /// The response headers.
255    pub headers: HeaderMap,
256    /// Up to 64 KiB of the response body, e.g. an error page from a proxy.
257    pub body: Bytes,
258}
259
260/// The error behind a [`CallFailure`].
261#[derive(Clone, Debug)]
262#[non_exhaustive]
263pub enum Cause {
264    /// The connection pool could not provide a connection.
265    Client(client::ClientError),
266    /// The HTTP/2 connection or stream failed.
267    Operation(OperationError),
268    /// The HTTP/2 stream failed.
269    Stream(StreamError),
270    /// The request could not be built, e.g. an invalid header.
271    Http(HttpError),
272    /// The response message could not be decoded.
273    Decode(DecodeError),
274}
275
276impl ClientError {
277    /// The gRPC code of the failure.
278    ///
279    /// The server's code for [`Status`](Self::Status), the code the HTTP
280    /// status maps to for [`NotGrpc`](Self::NotGrpc) and
281    /// [`CallFailure::code`] otherwise.
282    pub fn status(&self) -> GrpcStatus {
283        match self {
284            Self::Status(st) => st.code,
285            Self::NotGrpc(resp) => resp.status(),
286            Self::InvalidRequest(f)
287            | Self::NotProcessed(f)
288            | Self::Connection { failure: f, .. }
289            | Self::StreamFailed(f)
290            | Self::InvalidResponse(f)
291            | Self::DeadlineExceeded(f) => f.code,
292        }
293    }
294
295    /// What went wrong.
296    ///
297    /// The percent-decoded `grpc-message` of the server for
298    /// [`Status`](Self::Status), empty if it sent none. A description of the
299    /// failure otherwise.
300    pub fn message(&self) -> Cow<'_, str> {
301        match self {
302            Self::Status(st) => st.message(),
303            Self::NotGrpc(resp) => resp.message(),
304            Self::InvalidRequest(f)
305            | Self::NotProcessed(f)
306            | Self::Connection { failure: f, .. }
307            | Self::StreamFailed(f)
308            | Self::InvalidResponse(f)
309            | Self::DeadlineExceeded(f) => Cow::Borrowed(&f.message),
310        }
311    }
312}
313
314impl Status {
315    /// Create a status, `metadata` holds the trailers.
316    pub fn new(code: GrpcStatus, metadata: HeaderMap) -> Self {
317        Self { code, metadata }
318    }
319
320    /// The percent-decoded `grpc-message`, empty if the server sent none.
321    ///
322    /// Invalid escapes are kept as they are.
323    pub fn message(&self) -> Cow<'_, str> {
324        self.metadata
325            .get(consts::GRPC_MESSAGE)
326            .map_or(Cow::Borrowed(""), utils::percent_decode)
327    }
328}
329
330impl CallFailure {
331    /// Create a failure with no headers.
332    pub fn new(code: GrpcStatus, message: impl Into<ByteString>) -> Self {
333        Self {
334            code,
335            message: message.into(),
336            headers: HeaderMap::new(),
337            cause: None,
338        }
339    }
340
341    /// Set the response headers received before the failure.
342    #[must_use]
343    pub fn with_headers(mut self, headers: HeaderMap) -> Self {
344        self.headers = headers;
345        self
346    }
347
348    /// Set the error behind the failure.
349    #[must_use]
350    pub fn with_cause(mut self, cause: impl Into<Cause>) -> Self {
351        self.cause = Some(cause.into());
352        self
353    }
354}
355
356impl NotGrpc {
357    /// Create a response from something other than a grpc server.
358    pub fn new(http_status: Option<StatusCode>, headers: HeaderMap, body: Bytes) -> Self {
359        Self {
360            http_status,
361            headers,
362            body,
363        }
364    }
365
366    /// The gRPC code the HTTP status maps to, as the gRPC spec says.
367    ///
368    /// E.g. 404 is `UNIMPLEMENTED` and 503 is `UNAVAILABLE`. A 200 response
369    /// whose `content-type` is not grpc, or a response without status, is
370    /// `UNKNOWN`.
371    pub fn status(&self) -> GrpcStatus {
372        match self.http_status.map(|st| st.as_u16()) {
373            Some(400) => GrpcStatus::Internal,
374            Some(401) => GrpcStatus::Unauthenticated,
375            Some(403) => GrpcStatus::PermissionDenied,
376            Some(404) => GrpcStatus::Unimplemented,
377            Some(429 | 502 | 503 | 504) => GrpcStatus::Unavailable,
378            _ => GrpcStatus::Unknown,
379        }
380    }
381
382    /// Describes the response, e.g. "HTTP status 503".
383    pub fn message(&self) -> Cow<'_, str> {
384        match self.http_status {
385            None => Cow::Borrowed("Response has no HTTP status"),
386            Some(st) if st != StatusCode::OK => Cow::Owned(format!("HTTP status {}", st.as_u16())),
387            Some(_) => match self.headers.get(header::CONTENT_TYPE) {
388                None => Cow::Borrowed("Response has no content-type"),
389                Some(val) => Cow::Owned(format!(
390                    "Invalid content-type: {}",
391                    String::from_utf8_lossy(val.as_bytes())
392                )),
393            },
394        }
395    }
396}
397
398macro_rules! cause_from {
399    ($($var:ident($ty:ty)),+) => {
400        $(impl From<$ty> for Cause {
401            fn from(err: $ty) -> Self {
402                Cause::$var(err)
403            }
404        })+
405    };
406}
407
408cause_from!(
409    Client(client::ClientError),
410    Operation(OperationError),
411    Stream(StreamError),
412    Http(HttpError),
413    Decode(DecodeError)
414);
415
416impl fmt::Display for ClientError {
417    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418        match self {
419            Self::Status(st) => write!(f, "Server replied with {:?}: {}", st.code, st.message()),
420            Self::InvalidRequest(e) => write!(f, "Invalid request: {e}"),
421            Self::NotProcessed(e) => write!(f, "Request is not processed: {e}"),
422            Self::Connection { failure, .. } => write!(f, "Connection failed: {failure}"),
423            Self::StreamFailed(e) => write!(f, "Stream failed: {e}"),
424            Self::NotGrpc(resp) => write!(f, "Not a grpc response: {}", resp.message()),
425            Self::InvalidResponse(e) => write!(f, "Invalid response: {e}"),
426            Self::DeadlineExceeded(e) => write!(f, "Deadline exceeded: {e}"),
427        }
428    }
429}
430
431impl std::error::Error for ClientError {
432    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
433        match self {
434            Self::Status(_) | Self::NotGrpc(_) => None,
435            Self::InvalidRequest(f)
436            | Self::NotProcessed(f)
437            | Self::Connection { failure: f, .. }
438            | Self::StreamFailed(f)
439            | Self::InvalidResponse(f)
440            | Self::DeadlineExceeded(f) => f.source(),
441        }
442    }
443}
444
445impl fmt::Display for CallFailure {
446    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
447        f.write_str(&self.message)
448    }
449}
450
451impl std::error::Error for CallFailure {
452    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
453        match self.cause.as_ref()? {
454            Cause::Client(err) => Some(err),
455            Cause::Operation(err) => Some(err),
456            Cause::Stream(err) => Some(err),
457            Cause::Http(err) => Some(err),
458            Cause::Decode(err) => Some(err),
459        }
460    }
461}
462
463impl ErrorDiagnostic for ClientError {
464    fn signature(&self) -> &'static str {
465        match self {
466            Self::Status(st) => st.code.signature(),
467            Self::InvalidRequest(_) => "grpc-InvalidRequest",
468            Self::NotProcessed(_) => "grpc-NotProcessed",
469            Self::Connection { .. } => "grpc-Connection",
470            Self::StreamFailed(_) => "grpc-StreamFailed",
471            Self::NotGrpc(_) => "grpc-NotGrpc",
472            Self::InvalidResponse(_) => "grpc-InvalidResponse",
473            Self::DeadlineExceeded(_) => "grpc-BackendCallTimedout",
474        }
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use std::error::Error as _;
481
482    use ntex_http::HeaderValue;
483
484    use super::*;
485
486    #[test]
487    fn errors() {
488        let mut hdrs = HeaderMap::new();
489        hdrs.insert(consts::GRPC_MESSAGE, HeaderValue::from_static("a%20b"));
490        let failure = |code| CallFailure::new(code, "msg").with_headers(hdrs.clone());
491
492        let errors = [
493            (
494                ClientError::Status(Status::new(GrpcStatus::NotFound, hdrs.clone())),
495                "grpc-status-NotFound",
496                GrpcStatus::NotFound,
497                "Server replied with NotFound: a b",
498            ),
499            (
500                ClientError::InvalidRequest(
501                    failure(GrpcStatus::Internal)
502                        .with_cause(HttpError::from(HeaderValue::from_str("\n").unwrap_err())),
503                ),
504                "grpc-InvalidRequest",
505                GrpcStatus::Internal,
506                "Invalid request: msg",
507            ),
508            (
509                ClientError::NotProcessed(failure(GrpcStatus::Unavailable)),
510                "grpc-NotProcessed",
511                GrpcStatus::Unavailable,
512                "Request is not processed: msg",
513            ),
514            (
515                ClientError::Connection {
516                    request_sent: false,
517                    failure: failure(GrpcStatus::Unavailable)
518                        .with_cause(client::ClientError::HandshakeTimeout),
519                },
520                "grpc-Connection",
521                GrpcStatus::Unavailable,
522                "Connection failed: msg",
523            ),
524            (
525                ClientError::StreamFailed(
526                    failure(GrpcStatus::Cancelled).with_cause(StreamError::Closed),
527                ),
528                "grpc-StreamFailed",
529                GrpcStatus::Cancelled,
530                "Stream failed: msg",
531            ),
532            (
533                ClientError::NotGrpc(NotGrpc::new(
534                    Some(StatusCode::BAD_GATEWAY),
535                    hdrs.clone(),
536                    Bytes::from_static(b"body"),
537                )),
538                "grpc-NotGrpc",
539                GrpcStatus::Unavailable,
540                "Not a grpc response: HTTP status 502",
541            ),
542            (
543                ClientError::InvalidResponse(
544                    failure(GrpcStatus::Internal).with_cause(DecodeError::new("bad")),
545                ),
546                "grpc-InvalidResponse",
547                GrpcStatus::Internal,
548                "Invalid response: msg",
549            ),
550            (
551                ClientError::DeadlineExceeded(
552                    failure(GrpcStatus::DeadlineExceeded).with_cause(OperationError::Disconnected),
553                ),
554                "grpc-BackendCallTimedout",
555                GrpcStatus::DeadlineExceeded,
556                "Deadline exceeded: msg",
557            ),
558        ];
559
560        for (err, signature, status, display) in errors {
561            let copy = err.clone();
562            assert_eq!(copy.signature(), signature);
563            assert_eq!(copy.status(), status);
564            assert_eq!(copy.to_string(), display);
565            assert_eq!(format!("{copy:?}"), format!("{err:?}"));
566            assert_eq!(copy.message(), err.message());
567            // failures with a cause expose it
568            let has_source = !matches!(
569                err,
570                ClientError::Status(_) | ClientError::NotGrpc(_) | ClientError::NotProcessed(_)
571            );
572            assert_eq!(copy.source().is_some(), has_source, "{err:?}");
573        }
574
575        // only a server status has the server's message
576        let st = ClientError::Status(Status::new(GrpcStatus::Internal, hdrs.clone()));
577        assert_eq!(st.message(), "a b");
578        let st = ClientError::Status(Status::new(GrpcStatus::Internal, HeaderMap::new()));
579        assert_eq!(st.message(), "");
580        let err = ClientError::InvalidResponse(failure(GrpcStatus::Internal));
581        assert_eq!(err.message(), "msg");
582    }
583
584    #[test]
585    fn not_grpc() {
586        for (code, status) in [
587            (400, GrpcStatus::Internal),
588            (401, GrpcStatus::Unauthenticated),
589            (403, GrpcStatus::PermissionDenied),
590            (404, GrpcStatus::Unimplemented),
591            (429, GrpcStatus::Unavailable),
592            (502, GrpcStatus::Unavailable),
593            (503, GrpcStatus::Unavailable),
594            (504, GrpcStatus::Unavailable),
595            (500, GrpcStatus::Unknown),
596            (302, GrpcStatus::Unknown),
597            (204, GrpcStatus::Unknown),
598        ] {
599            let st = StatusCode::from_u16(code).unwrap();
600            let resp = NotGrpc::new(Some(st), HeaderMap::new(), Bytes::new());
601            assert_eq!(resp.status(), status, "{code}");
602            assert_eq!(resp.message(), format!("HTTP status {code}"));
603        }
604
605        let mut resp = NotGrpc::new(Some(StatusCode::OK), HeaderMap::new(), Bytes::new());
606        assert_eq!(resp.status(), GrpcStatus::Unknown);
607        assert_eq!(resp.message(), "Response has no content-type");
608        resp.headers
609            .insert(header::CONTENT_TYPE, HeaderValue::from_static("text/html"));
610        assert_eq!(resp.message(), "Invalid content-type: text/html");
611
612        let resp = NotGrpc::new(None, HeaderMap::new(), Bytes::new());
613        assert_eq!(resp.status(), GrpcStatus::Unknown);
614        assert_eq!(resp.message(), "Response has no HTTP status");
615    }
616}