1use crate::{agent::status::Status, RequestIdError};
4use ic_certification::Label;
5use ic_transport_types::{InvalidRejectCodeError, RejectResponse};
6use leb128::read;
7use std::time::Duration;
8use std::{
9 fmt::{Debug, Display, Formatter},
10 str::Utf8Error,
11};
12use thiserror::Error;
13
14#[derive(Error, Debug)]
16pub enum AgentError {
17 #[error(r#"Invalid Replica URL: "{0}""#)]
19 InvalidReplicaUrl(String),
20
21 #[error("The request timed out.")]
23 TimeoutWaitingForResponse(),
24
25 #[error("Identity had a signing error: {0}")]
27 SigningError(String),
28
29 #[error("Invalid CBOR data, could not deserialize: {0}")]
31 InvalidCborData(#[from] serde_cbor::Error),
32
33 #[error("Cannot calculate a RequestID: {0}")]
35 CannotCalculateRequestId(#[from] RequestIdError),
36
37 #[error("Candid returned an error: {0}")]
39 CandidError(Box<dyn Send + Sync + std::error::Error>),
40
41 #[error(r#"Cannot parse url: "{0}""#)]
43 UrlParseError(#[from] url::ParseError),
44
45 #[error(r#"Invalid method: "{0}""#)]
47 InvalidMethodError(#[from] http::method::InvalidMethod),
48
49 #[error("Cannot parse Principal: {0}")]
51 PrincipalError(#[from] crate::export::PrincipalError),
52
53 #[error("The replica returned a rejection error: reject code {:?}, reject message {}, error code {:?}", .0.reject_code, .0.reject_message, .0.error_code)]
55 CertifiedReject(RejectResponse),
56
57 #[error("The replica returned a rejection error: reject code {:?}, reject message {}, error code {:?}", .0.reject_code, .0.reject_message, .0.error_code)]
59 UncertifiedReject(RejectResponse),
60
61 #[error("The replica returned an HTTP Error: {0}")]
63 HttpError(HttpErrorPayload),
64
65 #[error("Status endpoint returned an invalid status.")]
67 InvalidReplicaStatus,
68
69 #[error("Call was marked as done but we never saw the reply. Request ID: {0}")]
71 RequestStatusDoneNoReply(String),
72
73 #[error("A tool returned a string message error: {0}")]
75 MessageError(String),
76
77 #[error("Error reading LEB128 value: {0}")]
79 Leb128ReadError(#[from] read::Error),
80
81 #[error("Error in UTF-8 string: {0}")]
83 Utf8ReadError(#[from] Utf8Error),
84
85 #[error("The lookup path ({0:?}) is absent in the certificate.")]
87 LookupPathAbsent(Vec<Label>),
88
89 #[error("The lookup path ({0:?}) is unknown in the certificate.")]
91 LookupPathUnknown(Vec<Label>),
92
93 #[error("The lookup path ({0:?}) does not make sense for the certificate.")]
95 LookupPathError(Vec<Label>),
96
97 #[error("The request status ({1}) at path {0:?} is invalid.")]
99 InvalidRequestStatus(Vec<Label>, String),
100
101 #[error("Certificate verification failed.")]
103 CertificateVerificationFailed(),
104
105 #[error("Query signature verification failed.")]
107 QuerySignatureVerificationFailed,
108
109 #[error("Certificate is not authorized to respond to queries for this canister. While developing: Did you forget to set effective_canister_id?")]
111 CertificateNotAuthorized(),
112
113 #[error("Certificate is stale (over {0:?}). Is the computer's clock synchronized?")]
115 CertificateOutdated(Duration),
116
117 #[error("The certificate contained more than one delegation")]
119 CertificateHasTooManyDelegations,
120
121 #[error("Query response did not contain any node signatures")]
123 MissingSignature,
124
125 #[error("Query response contained a malformed signature")]
127 MalformedSignature,
128
129 #[error("Read state response contained a malformed public key")]
131 MalformedPublicKey,
132
133 #[error("Query response contained too many signatures ({had}, exceeding the subnet's total nodes: {needed})")]
135 TooManySignatures {
136 had: usize,
138 needed: usize,
140 },
141
142 #[error(
144 r#"BLS DER-encoded public key must be ${expected} bytes long, but is {actual} bytes long."#
145 )]
146 DerKeyLengthMismatch {
147 expected: usize,
149 actual: usize,
151 },
152
153 #[error("BLS DER-encoded public key is invalid. Expected the following prefix: ${expected:?}, but got ${actual:?}")]
155 DerPrefixMismatch {
156 expected: Vec<u8>,
158 actual: Vec<u8>,
160 },
161
162 #[error("The status response did not contain a root key. Status: {0}")]
164 NoRootKeyInStatus(Status),
165
166 #[error("The invocation to the wallet call forward method failed with the error: {0}")]
168 WalletCallFailed(String),
169
170 #[error("The wallet operation failed: {0}")]
172 WalletError(String),
173
174 #[error("The wallet canister must be upgraded: {0}")]
176 WalletUpgradeRequired(String),
177
178 #[error("Response size exceeded limit.")]
180 ResponseSizeExceededLimit(),
181
182 #[error("An error happened during communication with the replica: {0}")]
184 TransportError(#[from] reqwest::Error),
185
186 #[error("There is a mismatch between the CBOR encoded call and the arguments: field {field}, value in argument is {value_arg}, value in CBOR is {value_cbor}")]
188 CallDataMismatch {
189 field: String,
191 value_arg: String,
193 value_cbor: String,
195 },
196
197 #[error(transparent)]
199 InvalidRejectCode(#[from] InvalidRejectCodeError),
200
201 #[error("Route provider failed to generate url: {0}")]
203 RouteProviderError(String),
204
205 #[error("Invalid HTTP response: {0}")]
207 InvalidHttpResponse(String),
208}
209
210impl PartialEq for AgentError {
211 fn eq(&self, other: &Self) -> bool {
212 format!("{self:?}") == format!("{other:?}")
215 }
216}
217
218impl From<candid::Error> for AgentError {
219 fn from(e: candid::Error) -> AgentError {
220 AgentError::CandidError(e.into())
221 }
222}
223
224pub struct HttpErrorPayload {
226 pub status: u16,
228 pub content_type: Option<String>,
230 pub content: Vec<u8>,
232}
233
234impl HttpErrorPayload {
235 fn fmt_human_readable(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
236 f.write_fmt(format_args!(
240 "Http Error: status {}, content type {:?}, content: {}",
241 http::StatusCode::from_u16(self.status)
242 .map_or_else(|_| format!("{}", self.status), |code| format!("{code}")),
243 self.content_type.clone().unwrap_or_default(),
244 String::from_utf8(self.content.clone()).unwrap_or_else(|_| format!(
245 "(unable to decode content as UTF-8: {:?})",
246 self.content
247 ))
248 ))?;
249 Ok(())
250 }
251}
252
253impl Debug for HttpErrorPayload {
254 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
255 self.fmt_human_readable(f)
256 }
257}
258
259impl Display for HttpErrorPayload {
260 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
261 self.fmt_human_readable(f)
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::HttpErrorPayload;
268 use crate::AgentError;
269
270 #[test]
271 fn content_type_none_valid_utf8() {
272 let payload = HttpErrorPayload {
273 status: 420,
274 content_type: None,
275 content: vec![104, 101, 108, 108, 111],
276 };
277
278 assert_eq!(
279 format!("{}", AgentError::HttpError(payload)),
280 r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "", content: hello"#,
281 );
282 }
283
284 #[test]
285 fn content_type_none_invalid_utf8() {
286 let payload = HttpErrorPayload {
287 status: 420,
288 content_type: None,
289 content: vec![195, 40],
290 };
291
292 assert_eq!(
293 format!("{}", AgentError::HttpError(payload)),
294 r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "", content: (unable to decode content as UTF-8: [195, 40])"#,
295 );
296 }
297
298 #[test]
299 fn formats_text_plain() {
300 let payload = HttpErrorPayload {
301 status: 420,
302 content_type: Some("text/plain".to_string()),
303 content: vec![104, 101, 108, 108, 111],
304 };
305
306 assert_eq!(
307 format!("{}", AgentError::HttpError(payload)),
308 r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "text/plain", content: hello"#,
309 );
310 }
311
312 #[test]
313 fn formats_text_plain_charset_utf8() {
314 let payload = HttpErrorPayload {
315 status: 420,
316 content_type: Some("text/plain; charset=utf-8".to_string()),
317 content: vec![104, 101, 108, 108, 111],
318 };
319
320 assert_eq!(
321 format!("{}", AgentError::HttpError(payload)),
322 r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "text/plain; charset=utf-8", content: hello"#,
323 );
324 }
325
326 #[test]
327 fn formats_text_html() {
328 let payload = HttpErrorPayload {
329 status: 420,
330 content_type: Some("text/html".to_string()),
331 content: vec![119, 111, 114, 108, 100],
332 };
333
334 assert_eq!(
335 format!("{}", AgentError::HttpError(payload)),
336 r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "text/html", content: world"#,
337 );
338 }
339}