ic_agent/agent/
agent_error.rs

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