cratestack_client_rust/rpc/
error.rs1use cratestack_core::{CoolError, rpc::RpcErrorBody};
2use reqwest::StatusCode;
3use serde::de::DeserializeOwned;
4
5use crate::codec::HttpClientCodec;
6use crate::error::ClientError;
7use crate::runtime::wire::RuntimeResponseWire;
8
9#[derive(Debug, Clone)]
14pub struct RpcRemoteError {
15 pub status: StatusCode,
16 pub body: RpcErrorBody,
17}
18
19impl std::fmt::Display for RpcRemoteError {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 write!(
22 f,
23 "RPC call failed with code {} (status {}): {}",
24 self.body.code,
25 self.status.as_u16(),
26 self.body.message
27 )
28 }
29}
30
31impl std::error::Error for RpcRemoteError {}
32
33#[derive(Debug, thiserror::Error)]
38pub enum RpcClientError {
39 #[error("transport error: {0}")]
40 Transport(#[from] reqwest::Error),
41 #[error("codec error: {0}")]
42 Codec(#[from] CoolError),
43 #[error("invalid response: {0}")]
44 InvalidResponse(String),
45 #[error("bad input: {0}")]
46 BadInput(String),
47 #[error("{0}")]
48 Remote(RpcRemoteError),
49}
50
51pub type RpcStream<O> = tokio::sync::mpsc::Receiver<Result<O, RpcClientError>>;
58
59pub(crate) fn http_status_for_rpc_code(code: &str) -> StatusCode {
65 match code {
66 "invalid_argument" => StatusCode::BAD_REQUEST,
67 "unauthenticated" => StatusCode::UNAUTHORIZED,
68 "permission_denied" => StatusCode::FORBIDDEN,
69 "not_found" => StatusCode::NOT_FOUND,
70 "conflict" => StatusCode::CONFLICT,
71 "failed_precondition" => StatusCode::PRECONDITION_FAILED,
72 _ => StatusCode::INTERNAL_SERVER_ERROR,
73 }
74}
75
76pub(crate) fn client_error_to_rpc(error: ClientError) -> RpcClientError {
77 match error {
78 ClientError::Transport(error) => RpcClientError::Transport(error),
79 ClientError::Codec(error) => RpcClientError::Codec(error),
80 ClientError::InvalidResponse(message) => RpcClientError::InvalidResponse(message),
81 ClientError::BadInput(message) => RpcClientError::BadInput(message),
82 ClientError::State(message) => RpcClientError::InvalidResponse(message),
83 ClientError::Remote {
84 status,
85 error,
86 message,
87 } => {
88 let body = error
93 .map(cratestack_core::rpc::RpcErrorBody::from_cool_response)
94 .unwrap_or_else(|| RpcErrorBody {
95 code: "internal".to_owned(),
96 message,
97 details: None,
98 });
99 RpcClientError::Remote(RpcRemoteError { status, body })
100 }
101 }
102}
103
104pub(crate) fn decode_rpc_unary_response<C, Output>(
105 codec: &C,
106 response: &RuntimeResponseWire,
107) -> Result<Output, RpcClientError>
108where
109 C: HttpClientCodec,
110 Output: DeserializeOwned,
111{
112 let content_type = response
113 .headers
114 .iter()
115 .find(|header| header.name.eq_ignore_ascii_case("content-type"))
116 .map(|header| header.value.as_str())
117 .ok_or_else(|| {
118 RpcClientError::InvalidResponse("response is missing Content-Type header".to_owned())
119 })?;
120
121 if (200..=299).contains(&response.status_code) {
122 codec
123 .decode_response::<Output>(content_type, &response.body)
124 .map_err(RpcClientError::Codec)
125 } else {
126 let body = codec
127 .decode_response::<RpcErrorBody>(content_type, &response.body)
128 .unwrap_or_else(|_| RpcErrorBody {
129 code: "internal".to_owned(),
130 message: format!(
131 "unexpected RPC error body for status {}",
132 response.status_code
133 ),
134 details: None,
135 });
136 Err(RpcClientError::Remote(RpcRemoteError {
137 status: StatusCode::from_u16(response.status_code)
138 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
139 body,
140 }))
141 }
142}