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