1use crate::{agent::status::Status, RequestIdError};
4use candid::Principal;
5use ic_certification::Label;
6use ic_transport_types::{InvalidRejectCodeError, RejectResponse};
7use leb128::read;
8use std::time::Duration;
9use std::{
10 fmt::{Debug, Display, Formatter},
11 str::Utf8Error,
12};
13use thiserror::Error;
14
15#[derive(Error, Debug)]
17pub enum AgentError {
18 #[error(r#"Invalid Replica URL: "{0}""#)]
20 InvalidReplicaUrl(String),
21
22 #[error("The request timed out.")]
24 TimeoutWaitingForResponse(),
25
26 #[error("Identity had a signing error: {0}")]
28 SigningError(String),
29
30 #[error("Invalid CBOR data, could not deserialize: {0}")]
32 InvalidCborData(#[from] serde_cbor::Error),
33
34 #[error("Cannot calculate a RequestID: {0}")]
36 CannotCalculateRequestId(#[from] RequestIdError),
37
38 #[error("Candid returned an error: {0}")]
40 CandidError(Box<dyn Send + Sync + std::error::Error>),
41
42 #[error(r#"Cannot parse url: "{0}""#)]
44 UrlParseError(#[from] url::ParseError),
45
46 #[error(r#"Invalid method: "{0}""#)]
48 InvalidMethodError(#[from] http::method::InvalidMethod),
49
50 #[error("Cannot parse Principal: {0}")]
52 PrincipalError(#[from] crate::export::PrincipalError),
53
54 #[error("The replica returned a rejection error: reject code {:?}, reject message {}, error code {:?}", .reject.reject_code, .reject.reject_message, .reject.error_code)]
56 CertifiedReject {
57 reject: RejectResponse,
59 operation: Option<Operation>,
61 },
62
63 #[error("The replica returned a rejection error: reject code {:?}, reject message {}, error code {:?}", .reject.reject_code, .reject.reject_message, .reject.error_code)]
65 UncertifiedReject {
66 reject: RejectResponse,
68 operation: Option<Operation>,
70 },
71
72 #[error("The replica returned an HTTP Error: {0}")]
74 HttpError(HttpErrorPayload),
75
76 #[error("Status endpoint returned an invalid status.")]
78 InvalidReplicaStatus,
79
80 #[error("Call was marked as done but we never saw the reply. Request ID: {0}")]
82 RequestStatusDoneNoReply(String),
83
84 #[error("A tool returned a string message error: {0}")]
86 MessageError(String),
87
88 #[error("Error reading LEB128 value: {0}")]
90 Leb128ReadError(#[from] read::Error),
91
92 #[error("Error in UTF-8 string: {0}")]
94 Utf8ReadError(#[from] Utf8Error),
95
96 #[error("The lookup path ({0:?}) is absent in the certificate.")]
98 LookupPathAbsent(Vec<Label>),
99
100 #[error("The lookup path ({0:?}) is unknown in the certificate.")]
102 LookupPathUnknown(Vec<Label>),
103
104 #[error("The lookup path ({0:?}) does not make sense for the certificate.")]
106 LookupPathError(Vec<Label>),
107
108 #[error("The request status ({1}) at path {0:?} is invalid.")]
110 InvalidRequestStatus(Vec<Label>, String),
111
112 #[error("Certificate verification failed.")]
114 CertificateVerificationFailed(),
115
116 #[error("Query signature verification failed.")]
118 QuerySignatureVerificationFailed,
119
120 #[error("Certificate is not authorized to respond to queries for this canister. While developing: Did you forget to set effective_canister_id?")]
122 CertificateNotAuthorized(),
123
124 #[error("Certificate is stale (over {0:?}). Is the computer's clock synchronized?")]
126 CertificateOutdated(Duration),
127
128 #[error("The certificate contained more than one delegation")]
130 CertificateHasTooManyDelegations,
131
132 #[error("Query response did not contain any node signatures")]
134 MissingSignature,
135
136 #[error("Query response contained a malformed signature")]
138 MalformedSignature,
139
140 #[error("Read state response contained a malformed public key")]
142 MalformedPublicKey,
143
144 #[error("Query response contained too many signatures ({had}, exceeding the subnet's total nodes: {needed})")]
146 TooManySignatures {
147 had: usize,
149 needed: usize,
151 },
152
153 #[error(
155 r#"BLS DER-encoded public key must be ${expected} bytes long, but is {actual} bytes long."#
156 )]
157 DerKeyLengthMismatch {
158 expected: usize,
160 actual: usize,
162 },
163
164 #[error("BLS DER-encoded public key is invalid. Expected the following prefix: ${expected:?}, but got ${actual:?}")]
166 DerPrefixMismatch {
167 expected: Vec<u8>,
169 actual: Vec<u8>,
171 },
172
173 #[error("The status response did not contain a root key. Status: {0}")]
175 NoRootKeyInStatus(Status),
176
177 #[error("The invocation to the wallet call forward method failed with the error: {0}")]
179 WalletCallFailed(String),
180
181 #[error("The wallet operation failed: {0}")]
183 WalletError(String),
184
185 #[error("The wallet canister must be upgraded: {0}")]
187 WalletUpgradeRequired(String),
188
189 #[error("Response size exceeded limit.")]
191 ResponseSizeExceededLimit(),
192
193 #[error("An error happened during communication with the replica: {0}")]
195 TransportError(#[from] reqwest::Error),
196
197 #[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}")]
199 CallDataMismatch {
200 field: String,
202 value_arg: String,
204 value_cbor: String,
206 },
207
208 #[error(transparent)]
210 InvalidRejectCode(#[from] InvalidRejectCodeError),
211
212 #[error("Route provider failed to generate url: {0}")]
214 RouteProviderError(String),
215
216 #[error("Invalid HTTP response: {0}")]
218 InvalidHttpResponse(String),
219}
220
221impl PartialEq for AgentError {
222 fn eq(&self, other: &Self) -> bool {
223 format!("{self:?}") == format!("{other:?}")
226 }
227}
228
229impl From<candid::Error> for AgentError {
230 fn from(e: candid::Error) -> AgentError {
231 AgentError::CandidError(e.into())
232 }
233}
234
235pub struct HttpErrorPayload {
237 pub status: u16,
239 pub content_type: Option<String>,
241 pub content: Vec<u8>,
243}
244
245impl HttpErrorPayload {
246 fn fmt_human_readable(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
247 f.write_fmt(format_args!(
251 "Http Error: status {}, content type {:?}, content: {}",
252 http::StatusCode::from_u16(self.status)
253 .map_or_else(|_| format!("{}", self.status), |code| format!("{code}")),
254 self.content_type.clone().unwrap_or_default(),
255 String::from_utf8(self.content.clone()).unwrap_or_else(|_| format!(
256 "(unable to decode content as UTF-8: {:?})",
257 self.content
258 ))
259 ))?;
260 Ok(())
261 }
262}
263
264impl Debug for HttpErrorPayload {
265 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
266 self.fmt_human_readable(f)
267 }
268}
269
270impl Display for HttpErrorPayload {
271 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
272 self.fmt_human_readable(f)
273 }
274}
275
276#[derive(Debug, Clone, Eq, PartialEq)]
278pub enum Operation {
279 Call {
281 canister: Principal,
283 method: String,
285 },
286 ReadState {
288 paths: Vec<Vec<String>>,
290 canister: Principal,
292 },
293 ReadSubnetState {
295 paths: Vec<Vec<String>>,
297 subnet: Principal,
299 },
300}
301
302#[cfg(test)]
303mod tests {
304 use super::HttpErrorPayload;
305 use crate::AgentError;
306
307 #[test]
308 fn content_type_none_valid_utf8() {
309 let payload = HttpErrorPayload {
310 status: 420,
311 content_type: None,
312 content: vec![104, 101, 108, 108, 111],
313 };
314
315 assert_eq!(
316 format!("{}", AgentError::HttpError(payload)),
317 r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "", content: hello"#,
318 );
319 }
320
321 #[test]
322 fn content_type_none_invalid_utf8() {
323 let payload = HttpErrorPayload {
324 status: 420,
325 content_type: None,
326 content: vec![195, 40],
327 };
328
329 assert_eq!(
330 format!("{}", AgentError::HttpError(payload)),
331 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])"#,
332 );
333 }
334
335 #[test]
336 fn formats_text_plain() {
337 let payload = HttpErrorPayload {
338 status: 420,
339 content_type: Some("text/plain".to_string()),
340 content: vec![104, 101, 108, 108, 111],
341 };
342
343 assert_eq!(
344 format!("{}", AgentError::HttpError(payload)),
345 r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "text/plain", content: hello"#,
346 );
347 }
348
349 #[test]
350 fn formats_text_plain_charset_utf8() {
351 let payload = HttpErrorPayload {
352 status: 420,
353 content_type: Some("text/plain; charset=utf-8".to_string()),
354 content: vec![104, 101, 108, 108, 111],
355 };
356
357 assert_eq!(
358 format!("{}", AgentError::HttpError(payload)),
359 r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "text/plain; charset=utf-8", content: hello"#,
360 );
361 }
362
363 #[test]
364 fn formats_text_html() {
365 let payload = HttpErrorPayload {
366 status: 420,
367 content_type: Some("text/html".to_string()),
368 content: vec![119, 111, 114, 108, 100],
369 };
370
371 assert_eq!(
372 format!("{}", AgentError::HttpError(payload)),
373 r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "text/html", content: world"#,
374 );
375 }
376}