1use base64::Engine as _;
9use base64::engine::general_purpose::STANDARD_NO_PAD;
10use connectrpc::{ConnectError, ErrorCode};
11use http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
12
13use crate::error::{Code, ServerError};
14use crate::upload::UploadError;
15
16fn error_code(code: Code) -> ErrorCode {
18 match code {
19 Code::Canceled => ErrorCode::Canceled,
20 Code::Unknown => ErrorCode::Unknown,
21 Code::InvalidArgument => ErrorCode::InvalidArgument,
22 Code::DeadlineExceeded => ErrorCode::DeadlineExceeded,
23 Code::NotFound => ErrorCode::NotFound,
24 Code::AlreadyExists => ErrorCode::AlreadyExists,
25 Code::PermissionDenied => ErrorCode::PermissionDenied,
26 Code::ResourceExhausted => ErrorCode::ResourceExhausted,
27 Code::FailedPrecondition => ErrorCode::FailedPrecondition,
28 Code::Aborted => ErrorCode::Aborted,
29 Code::OutOfRange => ErrorCode::OutOfRange,
30 Code::Unimplemented => ErrorCode::Unimplemented,
31 Code::Internal => ErrorCode::Internal,
32 Code::Unavailable => ErrorCode::Unavailable,
33 Code::DataLoss => ErrorCode::DataLoss,
34 Code::Unauthenticated => ErrorCode::Unauthenticated,
35 }
36}
37
38pub(super) fn recorded(err: &ConnectError) -> ServerError {
41 let code = match err.code {
42 ErrorCode::Canceled => Code::Canceled,
43 ErrorCode::InvalidArgument => Code::InvalidArgument,
44 ErrorCode::DeadlineExceeded => Code::DeadlineExceeded,
45 ErrorCode::NotFound => Code::NotFound,
46 ErrorCode::AlreadyExists => Code::AlreadyExists,
47 ErrorCode::PermissionDenied => Code::PermissionDenied,
48 ErrorCode::ResourceExhausted => Code::ResourceExhausted,
49 ErrorCode::FailedPrecondition => Code::FailedPrecondition,
50 ErrorCode::Aborted => Code::Aborted,
51 ErrorCode::OutOfRange => Code::OutOfRange,
52 ErrorCode::Unimplemented => Code::Unimplemented,
53 ErrorCode::Internal => Code::Internal,
54 ErrorCode::Unavailable => Code::Unavailable,
55 ErrorCode::DataLoss => Code::DataLoss,
56 ErrorCode::Unauthenticated => Code::Unauthenticated,
57 _ => Code::Unknown,
59 };
60 ServerError::new(code, err.message.clone().unwrap_or_default())
61}
62
63fn header_map(err: &ServerError) -> HeaderMap {
67 let mut map = HeaderMap::new();
68 for (name, value) in err.headers() {
69 let encoded = HeaderName::from_bytes(name.as_bytes())
70 .ok()
71 .zip(HeaderValue::from_str(value).ok());
72 if let Some((name, value)) = encoded {
73 map.append(name, value);
74 } else {
75 tracing::warn!(header = ?name, "dropped an unencodable error response header");
76 }
77 }
78 map
79}
80
81impl From<ServerError> for ConnectError {
82 fn from(err: ServerError) -> Self {
83 let mut out = ConnectError::new(error_code(err.code()), err.public_message());
84 if let Some(status) = err.http_status().and_then(|s| StatusCode::from_u16(s).ok()) {
85 out = out.with_http_status(status);
86 }
87 if !err.headers().is_empty() {
88 out = out.with_headers(header_map(&err));
89 }
90 for detail in err.details() {
91 out = out.with_detail(connectrpc::ErrorDetail {
92 type_url: detail.type_name.clone(),
93 value: Some(STANDARD_NO_PAD.encode(&detail.value)),
94 debug: None,
95 });
96 }
97 out
98 }
99}
100
101#[must_use]
104pub fn from_upload_error(err: UploadError) -> ConnectError {
105 ServerError::from(err).into()
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::error::ErrorDetail;
112
113 #[test]
114 fn every_code_keeps_its_name() {
115 let codes = [
116 Code::Canceled,
117 Code::Unknown,
118 Code::InvalidArgument,
119 Code::DeadlineExceeded,
120 Code::NotFound,
121 Code::AlreadyExists,
122 Code::PermissionDenied,
123 Code::ResourceExhausted,
124 Code::FailedPrecondition,
125 Code::Aborted,
126 Code::OutOfRange,
127 Code::Unimplemented,
128 Code::Internal,
129 Code::Unavailable,
130 Code::DataLoss,
131 Code::Unauthenticated,
132 ];
133 for code in codes {
134 assert_eq!(error_code(code).as_str(), code.as_str());
135 let back = recorded(&ConnectError::new(error_code(code), "m"));
136 assert_eq!(back.code(), code);
137 }
138 }
139
140 #[test]
141 fn internal_detail_never_crosses() {
142 let err: ConnectError = ServerError::internal("storage failure", "bucket=secret").into();
143 assert_eq!(err.code, ErrorCode::Internal);
144 assert_eq!(err.message.as_deref(), Some("storage failure"));
145 let json = String::from_utf8(err.to_json().to_vec()).unwrap();
146 assert!(!json.contains("secret"), "{json}");
147 assert!(err.details.is_empty());
148 }
149
150 #[test]
151 fn shaping_crosses() {
152 let err: ConnectError = ServerError::permission_denied("pay")
153 .with_http_status(402)
154 .with_header("WWW-Authenticate", "Payment id=a")
155 .with_header("WWW-Authenticate", "Payment id=b")
156 .with_detail(ErrorDetail {
157 type_name: "mkit.test.Detail".to_owned(),
158 value: bytes::Bytes::from_static(&[1, 2, 3]),
159 })
160 .into();
161 assert_eq!(err.http_status(), StatusCode::PAYMENT_REQUIRED);
162 let values: Vec<_> = err
163 .response_headers()
164 .get_all("www-authenticate")
165 .iter()
166 .collect();
167 assert_eq!(values, ["Payment id=a", "Payment id=b"]);
168 assert_eq!(err.details.len(), 1);
169 assert_eq!(err.details[0].type_url, "mkit.test.Detail");
170 assert_eq!(err.details[0].value.as_deref(), Some("AQID"));
171 }
172
173 #[test]
174 fn upload_errors_keep_the_connect_message() {
175 let err = from_upload_error(UploadError::HeaderMissing {
176 stream_empty: false,
177 });
178 assert_eq!(err.code, ErrorCode::InvalidArgument);
179 assert_eq!(
180 err.message.as_deref(),
181 Some("UploadPack: first message MUST be `header`")
182 );
183 let big = from_upload_error(UploadError::TotalTooLarge { total: 9, cap: 8 });
184 assert_eq!(big.code, ErrorCode::ResourceExhausted);
185 }
186}