Skip to main content

ntex_grpc/server/
error.rs

1use ntex_http::{HeaderMap, HeaderValue};
2
3use super::Response;
4use crate::{DecodeError, GrpcStatus};
5
6/// Error answer to a call.
7///
8/// It is sent as trailers: `grpc-status`, `grpc-message` and any extra
9/// headers. Hand-written services return it directly. With `#[server]`, a
10/// method returns it to fail the call (see [`MethodResult`] and
11/// [`StreamResult`]); it is also produced for unknown methods and requests
12/// that cannot be decoded.
13#[derive(thiserror::Error, Clone, Debug)]
14#[error("{status:?}: {message:?}")]
15pub struct ServerError {
16    pub(crate) status: GrpcStatus,
17    pub(crate) message: HeaderValue,
18    pub(crate) headers: HeaderMap,
19}
20
21impl ServerError {
22    /// Create an error from a status, a `grpc-message` text and extra
23    /// trailers.
24    ///
25    /// The message is sent as is. The gRPC spec expects it to be
26    /// percent-encoded, build it with
27    /// [`encode_grpc_message()`](crate::encode_grpc_message).
28    pub fn new(status: GrpcStatus, message: HeaderValue, headers: Option<HeaderMap>) -> Self {
29        Self {
30            status,
31            message,
32            headers: headers.unwrap_or_default(),
33        }
34    }
35}
36
37impl From<DecodeError> for ServerError {
38    fn from(_: DecodeError) -> Self {
39        Self::new(
40            GrpcStatus::InvalidArgument,
41            HeaderValue::from_static("Cannot decode grpc message"),
42            None,
43        )
44    }
45}
46
47/// Return types a unary or client-streaming `#[server]` method can have.
48///
49/// - `Reply`, the reply message.
50/// - `Result<Reply, E>` where `E: Into<Reply>`. The error is turned into a
51///   reply message and sent like any other reply, with status `OK`.
52/// - [`Response<Reply>`](super::Response), the reply message plus extra
53///   trailers.
54/// - `Result<Response<Reply>, E>` where `E: Into<ServerError>`. The error
55///   ends the call with its status, `grpc-message` and trailers.
56pub trait MethodResult<T> {
57    /// Turn into the reply, or the error the call ends with.
58    ///
59    /// # Errors
60    ///
61    /// Returns the error the call ends with.
62    fn into_response(self) -> Result<Response<T>, ServerError>;
63}
64
65impl<T> MethodResult<T> for T {
66    #[inline]
67    fn into_response(self) -> Result<Response<T>, ServerError> {
68        Ok(Response::new(self))
69    }
70}
71
72impl<T, E: Into<T>> MethodResult<T> for Result<T, E> {
73    #[inline]
74    fn into_response(self) -> Result<Response<T>, ServerError> {
75        match self {
76            Ok(res) => Ok(Response::new(res)),
77            Err(e) => Ok(Response::new(e.into())),
78        }
79    }
80}
81
82impl<T> MethodResult<T> for Response<T> {
83    #[inline]
84    fn into_response(self) -> Result<Response<T>, ServerError> {
85        Ok(self)
86    }
87}
88
89impl<T, E: Into<ServerError>> MethodResult<T> for Result<Response<T>, E> {
90    #[inline]
91    fn into_response(self) -> Result<Response<T>, ServerError> {
92        self.map_err(Into::into)
93    }
94}
95
96/// Return types a `#[server]` streaming method can have.
97///
98/// A bidirectional or server-streaming method returns `()`, or
99/// `Result<(), E>` where `E: Into<ServerError>`. The error is sent as the
100/// status of the call.
101pub trait StreamResult {
102    /// Turn into the result of the call.
103    ///
104    /// # Errors
105    ///
106    /// Returns the error the call ends with.
107    fn into_result(self) -> Result<(), ServerError>;
108}
109
110impl StreamResult for () {
111    #[inline]
112    fn into_result(self) -> Result<(), ServerError> {
113        Ok(())
114    }
115}
116
117impl<E: Into<ServerError>> StreamResult for Result<(), E> {
118    #[inline]
119    fn into_result(self) -> Result<(), ServerError> {
120        self.map_err(Into::into)
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use ntex_http::HeaderName;
127
128    use super::*;
129
130    #[test]
131    fn server_error() {
132        let err = ServerError::new(GrpcStatus::NotFound, HeaderValue::from_static("nope"), None);
133        assert_eq!(err.status, GrpcStatus::NotFound);
134        assert_eq!(err.message, HeaderValue::from_static("nope"));
135        assert!(err.headers.is_empty());
136
137        let text = err.to_string();
138        assert!(text.contains("NotFound"), "{text}");
139        assert!(text.contains("nope"), "{text}");
140        assert!(format!("{err:?}").contains("NotFound"));
141    }
142
143    #[test]
144    fn server_error_with_headers() {
145        let mut headers = HeaderMap::new();
146        headers.insert(
147            HeaderName::from_static("x-test"),
148            HeaderValue::from_static("1"),
149        );
150
151        let err = ServerError::new(
152            GrpcStatus::Internal,
153            HeaderValue::from_static("boom"),
154            Some(headers),
155        );
156        let err = err.clone();
157        assert_eq!(err.status, GrpcStatus::Internal);
158        assert_eq!(
159            err.headers.get("x-test").unwrap(),
160            &HeaderValue::from_static("1")
161        );
162    }
163
164    #[test]
165    fn from_decode_error() {
166        let err = ServerError::from(DecodeError::new("bad data"));
167        assert_eq!(err.status, GrpcStatus::InvalidArgument);
168        assert_eq!(
169            err.message,
170            HeaderValue::from_static("Cannot decode grpc message")
171        );
172        assert!(err.headers.is_empty());
173    }
174
175    /// STATEMENT: Every `MethodResult` return type turns into the reply or
176    /// the error the call ends with.
177    ///
178    /// HOW:
179    /// - Convert a plain reply and both arms of `Result<Reply, E: Into<Reply>>`,
180    ///   and confirm each is an `OK` reply with no trailers.
181    /// - Convert a `Response` with a trailer and confirm the trailer is kept.
182    /// - Convert both arms of `Result<Response<Reply>, ServerError>` and
183    ///   confirm the error keeps its status.
184    #[test]
185    fn method_result_conversion() {
186        fn reply<R: MethodResult<u32>>(res: R) -> u32 {
187            let res = res.into_response().unwrap();
188            assert!(res.headers.is_empty());
189            res.message
190        }
191
192        assert_eq!(reply(5), 5);
193        assert_eq!(reply(Ok::<u32, u8>(7)), 7);
194        assert_eq!(reply(Err::<u32, u8>(3)), 3);
195
196        let res = Response {
197            message: 9_u32,
198            headers: vec![(
199                HeaderName::from_static("x-test"),
200                HeaderValue::from_static("1"),
201            )],
202        };
203        let res = MethodResult::<u32>::into_response(res).unwrap();
204        assert_eq!(res.message, 9);
205        assert_eq!(res.headers.len(), 1);
206
207        let ok: Result<Response<u32>, ServerError> = Ok(Response::new(4));
208        assert_eq!(MethodResult::<u32>::into_response(ok).unwrap().message, 4);
209
210        let err: Result<Response<u32>, ServerError> = Err(ServerError::new(
211            GrpcStatus::NotFound,
212            HeaderValue::from_static("nope"),
213            None,
214        ));
215        let err = MethodResult::<u32>::into_response(err).err().unwrap();
216        assert_eq!(err.status, GrpcStatus::NotFound);
217    }
218}