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 TransportError {
18 #[error("{0}")]
20 Reqwest(reqwest::Error),
21 #[error("{0}")]
22 Generic(String),
24}
25
26#[derive(Error, Debug)]
28pub enum AgentError {
29 #[error(r#"Invalid Replica URL: "{0}""#)]
31 InvalidReplicaUrl(String),
32
33 #[error("The request timed out.")]
35 TimeoutWaitingForResponse(),
36
37 #[error("Identity had a signing error: {0}")]
39 SigningError(String),
40
41 #[error("Invalid CBOR data, could not deserialize: {0}")]
43 InvalidCborData(#[from] serde_cbor::Error),
44
45 #[error("Cannot calculate a RequestID: {0}")]
47 CannotCalculateRequestId(#[from] RequestIdError),
48
49 #[error("Candid returned an error: {0}")]
51 CandidError(Box<dyn Send + Sync + std::error::Error>),
52
53 #[error(r#"Cannot parse url: "{0}""#)]
55 UrlParseError(#[from] url::ParseError),
56
57 #[error(r#"Invalid method: "{0}""#)]
59 InvalidMethodError(#[from] http::method::InvalidMethod),
60
61 #[error("Cannot parse Principal: {0}")]
63 PrincipalError(#[from] crate::export::PrincipalError),
64
65 #[error("The replica returned a rejection error: reject code {:?}, reject message {}, error code {:?}", .reject.reject_code, .reject.reject_message, .reject.error_code)]
67 CertifiedReject {
68 reject: RejectResponse,
70 operation: Option<Operation>,
72 },
73
74 #[error("The replica returned a rejection error: reject code {:?}, reject message {}, error code {:?}", .reject.reject_code, .reject.reject_message, .reject.error_code)]
76 UncertifiedReject {
77 reject: RejectResponse,
79 operation: Option<Operation>,
81 },
82
83 #[error("The replica returned an HTTP Error: {0}")]
85 HttpError(HttpErrorPayload),
86
87 #[error("Status endpoint returned an invalid status.")]
89 InvalidReplicaStatus,
90
91 #[error("Call was marked as done but we never saw the reply. Request ID: {0}")]
93 RequestStatusDoneNoReply(String),
94
95 #[error("A tool returned a string message error: {0}")]
97 MessageError(String),
98
99 #[error("Error reading LEB128 value: {0}")]
101 Leb128ReadError(#[from] read::Error),
102
103 #[error("Error in UTF-8 string: {0}")]
105 Utf8ReadError(#[from] Utf8Error),
106
107 #[error("The lookup path ({0:?}) is absent in the certificate.")]
109 LookupPathAbsent(Vec<Label>),
110
111 #[error("The lookup path ({0:?}) is unknown in the certificate.")]
113 LookupPathUnknown(Vec<Label>),
114
115 #[error("The lookup path ({0:?}) does not make sense for the certificate.")]
117 LookupPathError(Vec<Label>),
118
119 #[error("The request status ({1}) at path {0:?} is invalid.")]
121 InvalidRequestStatus(Vec<Label>, String),
122
123 #[error("Certificate verification failed.")]
125 CertificateVerificationFailed(),
126
127 #[error("Query signature verification failed.")]
129 QuerySignatureVerificationFailed,
130
131 #[error("Certificate is not authorized to respond to queries for this canister. While developing: Did you forget to set effective_canister_id?")]
133 CertificateNotAuthorized(),
134
135 #[error("Certificate is stale (over {0:?}). Is the computer's clock synchronized?")]
137 CertificateOutdated(Duration),
138
139 #[error("The certificate contained more than one delegation")]
141 CertificateHasTooManyDelegations,
142
143 #[error("Query response did not contain any node signatures")]
145 MissingSignature,
146
147 #[error("Query response contained a malformed signature")]
149 MalformedSignature,
150
151 #[error("Read state response contained a malformed public key")]
153 MalformedPublicKey,
154
155 #[error("Query response contained too many signatures ({had}, exceeding the subnet's total nodes: {needed})")]
157 TooManySignatures {
158 had: usize,
160 needed: usize,
162 },
163
164 #[error(
166 r#"BLS DER-encoded public key must be ${expected} bytes long, but is {actual} bytes long."#
167 )]
168 DerKeyLengthMismatch {
169 expected: usize,
171 actual: usize,
173 },
174
175 #[error("BLS DER-encoded public key is invalid. Expected the following prefix: ${expected:?}, but got ${actual:?}")]
177 DerPrefixMismatch {
178 expected: Vec<u8>,
180 actual: Vec<u8>,
182 },
183
184 #[error("The status response did not contain a root key. Status: {0}")]
186 NoRootKeyInStatus(Status),
187
188 #[error("The invocation to the wallet call forward method failed with the error: {0}")]
190 WalletCallFailed(String),
191
192 #[error("The wallet operation failed: {0}")]
194 WalletError(String),
195
196 #[error("The wallet canister must be upgraded: {0}")]
198 WalletUpgradeRequired(String),
199
200 #[error("Response size exceeded limit.")]
202 ResponseSizeExceededLimit(),
203
204 #[error("An error happened during communication with the replica: {0}")]
206 TransportError(TransportError),
207
208 #[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}")]
210 CallDataMismatch {
211 field: String,
213 value_arg: String,
215 value_cbor: String,
217 },
218
219 #[error(transparent)]
221 InvalidRejectCode(#[from] InvalidRejectCodeError),
222
223 #[error("Route provider failed to generate url: {0}")]
225 RouteProviderError(String),
226
227 #[error("Invalid HTTP response: {0}")]
229 InvalidHttpResponse(String),
230}
231
232impl PartialEq for AgentError {
233 fn eq(&self, other: &Self) -> bool {
234 format!("{self:?}") == format!("{other:?}")
237 }
238}
239
240impl From<candid::Error> for AgentError {
241 fn from(e: candid::Error) -> AgentError {
242 AgentError::CandidError(e.into())
243 }
244}
245
246pub struct HttpErrorPayload {
248 pub status: u16,
250 pub content_type: Option<String>,
252 pub content: Vec<u8>,
254}
255
256impl HttpErrorPayload {
257 fn fmt_human_readable(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
258 f.write_fmt(format_args!(
262 "Http Error: status {}, content type {:?}, content: {}",
263 http::StatusCode::from_u16(self.status)
264 .map_or_else(|_| format!("{}", self.status), |code| format!("{code}")),
265 self.content_type.clone().unwrap_or_default(),
266 String::from_utf8(self.content.clone()).unwrap_or_else(|_| format!(
267 "(unable to decode content as UTF-8: {:?})",
268 self.content
269 ))
270 ))?;
271 Ok(())
272 }
273}
274
275impl Debug for HttpErrorPayload {
276 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
277 self.fmt_human_readable(f)
278 }
279}
280
281impl Display for HttpErrorPayload {
282 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
283 self.fmt_human_readable(f)
284 }
285}
286
287#[derive(Debug, Clone, Eq, PartialEq)]
289pub enum Operation {
290 Call {
292 canister: Principal,
294 method: String,
296 },
297 ReadState {
299 paths: Vec<Vec<String>>,
301 canister: Principal,
303 },
304 ReadSubnetState {
306 paths: Vec<Vec<String>>,
308 subnet: Principal,
310 },
311}
312
313#[cfg(test)]
314mod tests {
315 use super::HttpErrorPayload;
316 use crate::AgentError;
317
318 #[test]
319 fn content_type_none_valid_utf8() {
320 let payload = HttpErrorPayload {
321 status: 420,
322 content_type: None,
323 content: vec![104, 101, 108, 108, 111],
324 };
325
326 assert_eq!(
327 format!("{}", AgentError::HttpError(payload)),
328 r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "", content: hello"#,
329 );
330 }
331
332 #[test]
333 fn content_type_none_invalid_utf8() {
334 let payload = HttpErrorPayload {
335 status: 420,
336 content_type: None,
337 content: vec![195, 40],
338 };
339
340 assert_eq!(
341 format!("{}", AgentError::HttpError(payload)),
342 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])"#,
343 );
344 }
345
346 #[test]
347 fn formats_text_plain() {
348 let payload = HttpErrorPayload {
349 status: 420,
350 content_type: Some("text/plain".to_string()),
351 content: vec![104, 101, 108, 108, 111],
352 };
353
354 assert_eq!(
355 format!("{}", AgentError::HttpError(payload)),
356 r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "text/plain", content: hello"#,
357 );
358 }
359
360 #[test]
361 fn formats_text_plain_charset_utf8() {
362 let payload = HttpErrorPayload {
363 status: 420,
364 content_type: Some("text/plain; charset=utf-8".to_string()),
365 content: vec![104, 101, 108, 108, 111],
366 };
367
368 assert_eq!(
369 format!("{}", AgentError::HttpError(payload)),
370 r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "text/plain; charset=utf-8", content: hello"#,
371 );
372 }
373
374 #[test]
375 fn formats_text_html() {
376 let payload = HttpErrorPayload {
377 status: 420,
378 content_type: Some("text/html".to_string()),
379 content: vec![119, 111, 114, 108, 100],
380 };
381
382 assert_eq!(
383 format!("{}", AgentError::HttpError(payload)),
384 r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "text/html", content: world"#,
385 );
386 }
387}