ic_agent/agent/
agent_error.rs

1//! Errors that can occur when using the replica agent.
2
3use 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/// An error that occurs on transport layer
16#[derive(Error, Debug)]
17pub enum TransportError {
18    /// Reqwest-related error
19    #[error("{0}")]
20    Reqwest(reqwest::Error),
21    #[error("{0}")]
22    /// Generic non-specific error
23    Generic(String),
24}
25
26/// An error that occurred when using the agent.
27#[derive(Error, Debug)]
28pub enum AgentError {
29    /// The replica URL was invalid.
30    #[error(r#"Invalid Replica URL: "{0}""#)]
31    InvalidReplicaUrl(String),
32
33    /// The request timed out.
34    #[error("The request timed out.")]
35    TimeoutWaitingForResponse(),
36
37    /// An error occurred when signing with the identity.
38    #[error("Identity had a signing error: {0}")]
39    SigningError(String),
40
41    /// The data fetched was invalid CBOR.
42    #[error("Invalid CBOR data, could not deserialize: {0}")]
43    InvalidCborData(#[from] serde_cbor::Error),
44
45    /// There was an error calculating a request ID.
46    #[error("Cannot calculate a RequestID: {0}")]
47    CannotCalculateRequestId(#[from] RequestIdError),
48
49    /// There was an error when de/serializing with Candid.
50    #[error("Candid returned an error: {0}")]
51    CandidError(Box<dyn Send + Sync + std::error::Error>),
52
53    /// There was an error parsing a URL.
54    #[error(r#"Cannot parse url: "{0}""#)]
55    UrlParseError(#[from] url::ParseError),
56
57    /// The HTTP method was invalid.
58    #[error(r#"Invalid method: "{0}""#)]
59    InvalidMethodError(#[from] http::method::InvalidMethod),
60
61    /// The principal string was not a valid principal.
62    #[error("Cannot parse Principal: {0}")]
63    PrincipalError(#[from] crate::export::PrincipalError),
64
65    /// The subnet rejected the message.
66    #[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        /// The rejection returned by the replica.
69        reject: RejectResponse,
70        /// The operation that was rejected. Not always available.
71        operation: Option<Operation>,
72    },
73
74    /// The subnet may have rejected the message. This rejection cannot be verified as authentic.
75    #[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        /// The rejection returned by the boundary node.
78        reject: RejectResponse,
79        /// The operation that was rejected. Not always available.
80        operation: Option<Operation>,
81    },
82
83    /// The replica returned an HTTP error.
84    #[error("The replica returned an HTTP Error: {0}")]
85    HttpError(HttpErrorPayload),
86
87    /// The status endpoint returned an invalid status.
88    #[error("Status endpoint returned an invalid status.")]
89    InvalidReplicaStatus,
90
91    /// The call was marked done, but no reply was provided.
92    #[error("Call was marked as done but we never saw the reply. Request ID: {0}")]
93    RequestStatusDoneNoReply(String),
94
95    /// A string error occurred in an external tool.
96    #[error("A tool returned a string message error: {0}")]
97    MessageError(String),
98
99    /// There was an error reading a LEB128 value.
100    #[error("Error reading LEB128 value: {0}")]
101    Leb128ReadError(#[from] read::Error),
102
103    /// A string was invalid UTF-8.
104    #[error("Error in UTF-8 string: {0}")]
105    Utf8ReadError(#[from] Utf8Error),
106
107    /// The lookup path was absent in the certificate.
108    #[error("The lookup path ({0:?}) is absent in the certificate.")]
109    LookupPathAbsent(Vec<Label>),
110
111    /// The lookup path was unknown in the certificate.
112    #[error("The lookup path ({0:?}) is unknown in the certificate.")]
113    LookupPathUnknown(Vec<Label>),
114
115    /// The lookup path did not make sense for the certificate.
116    #[error("The lookup path ({0:?}) does not make sense for the certificate.")]
117    LookupPathError(Vec<Label>),
118
119    /// The request status at the requested path was invalid.
120    #[error("The request status ({1}) at path {0:?} is invalid.")]
121    InvalidRequestStatus(Vec<Label>, String),
122
123    /// The certificate verification for a `read_state` call failed.
124    #[error("Certificate verification failed.")]
125    CertificateVerificationFailed(),
126
127    /// The signature verification for a query call failed.
128    #[error("Query signature verification failed.")]
129    QuerySignatureVerificationFailed,
130
131    /// The certificate contained a delegation that does not include the `effective_canister_id` in the `canister_ranges` field.
132    #[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    /// The certificate was older than allowed by the `ingress_expiry`.
136    #[error("Certificate is stale (over {0:?}). Is the computer's clock synchronized?")]
137    CertificateOutdated(Duration),
138
139    /// The certificate contained more than one delegation.
140    #[error("The certificate contained more than one delegation")]
141    CertificateHasTooManyDelegations,
142
143    /// The query response did not contain any node signatures.
144    #[error("Query response did not contain any node signatures")]
145    MissingSignature,
146
147    /// The query response contained a malformed signature.
148    #[error("Query response contained a malformed signature")]
149    MalformedSignature,
150
151    /// The read-state response contained a malformed public key.
152    #[error("Read state response contained a malformed public key")]
153    MalformedPublicKey,
154
155    /// The query response contained more node signatures than the subnet has nodes.
156    #[error("Query response contained too many signatures ({had}, exceeding the subnet's total nodes: {needed})")]
157    TooManySignatures {
158        /// The number of provided signatures.
159        had: usize,
160        /// The number of nodes on the subnet.
161        needed: usize,
162    },
163
164    /// There was a length mismatch between the expected and actual length of the BLS DER-encoded public key.
165    #[error(
166        r#"BLS DER-encoded public key must be ${expected} bytes long, but is {actual} bytes long."#
167    )]
168    DerKeyLengthMismatch {
169        /// The expected length of the key.
170        expected: usize,
171        /// The actual length of the key.
172        actual: usize,
173    },
174
175    /// There was a mismatch between the expected and actual prefix of the BLS DER-encoded public key.
176    #[error("BLS DER-encoded public key is invalid. Expected the following prefix: ${expected:?}, but got ${actual:?}")]
177    DerPrefixMismatch {
178        /// The expected key prefix.
179        expected: Vec<u8>,
180        /// The actual key prefix.
181        actual: Vec<u8>,
182    },
183
184    /// The status response did not contain a root key.
185    #[error("The status response did not contain a root key.  Status: {0}")]
186    NoRootKeyInStatus(Status),
187
188    /// The invocation to the wallet call forward method failed with an error.
189    #[error("The invocation to the wallet call forward method failed with the error: {0}")]
190    WalletCallFailed(String),
191
192    /// The wallet operation failed.
193    #[error("The  wallet operation failed: {0}")]
194    WalletError(String),
195
196    /// The wallet canister must be upgraded. See [`dfx wallet upgrade`](https://internetcomputer.org/docs/current/references/cli-reference/dfx-wallet)
197    #[error("The wallet canister must be upgraded: {0}")]
198    WalletUpgradeRequired(String),
199
200    /// The response size exceeded the provided limit.
201    #[error("Response size exceeded limit.")]
202    ResponseSizeExceededLimit(),
203
204    /// An unknown error occurred during communication with the replica.
205    #[error("An error happened during communication with the replica: {0}")]
206    TransportError(TransportError),
207
208    /// There was a mismatch between the expected and actual CBOR data during inspection.
209    #[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        /// The field that was mismatched.
212        field: String,
213        /// The value that was expected to be in the CBOR.
214        value_arg: String,
215        /// The value that was actually in the CBOR.
216        value_cbor: String,
217    },
218
219    /// The rejected call had an invalid reject code (valid range 1..5).
220    #[error(transparent)]
221    InvalidRejectCode(#[from] InvalidRejectCodeError),
222
223    /// Route provider failed to generate a url for some reason.
224    #[error("Route provider failed to generate url: {0}")]
225    RouteProviderError(String),
226
227    /// Invalid HTTP response.
228    #[error("Invalid HTTP response: {0}")]
229    InvalidHttpResponse(String),
230}
231
232impl PartialEq for AgentError {
233    fn eq(&self, other: &Self) -> bool {
234        // Verify the debug string is the same. Some of the subtypes of this error
235        // don't implement Eq or PartialEq, so we cannot rely on derive.
236        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
246/// A HTTP error from the replica.
247pub struct HttpErrorPayload {
248    /// The HTTP status code.
249    pub status: u16,
250    /// The MIME type of `content`.
251    pub content_type: Option<String>,
252    /// The body of the error.
253    pub content: Vec<u8>,
254}
255
256impl HttpErrorPayload {
257    fn fmt_human_readable(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
258        // No matter content_type is TEXT or not,
259        // always try to parse it as a String.
260        // When fail, print the raw byte array
261        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/// An operation that can result in a reject.
288#[derive(Debug, Clone, Eq, PartialEq)]
289pub enum Operation {
290    /// A call to a canister method.
291    Call {
292        /// The canister whose method was called.
293        canister: Principal,
294        /// The name of the method.
295        method: String,
296    },
297    /// A read of the state tree, in the context of a canister. This will *not* be returned for request polling.
298    ReadState {
299        /// The requested paths within the state tree.
300        paths: Vec<Vec<String>>,
301        /// The canister the read request was made in the context of.
302        canister: Principal,
303    },
304    /// A read of the state tree, in the context of a subnet.
305    ReadSubnetState {
306        /// The requested paths within the state tree.
307        paths: Vec<Vec<String>>,
308        /// The subnet the read request was made in the context of.
309        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}