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}