ex3_ic_agent/agent/
agent_error.rs

1//! Errors that can occur when using the replica agent.
2
3use crate::{
4    agent::{replica_api::RejectResponse, status::Status},
5    RequestIdError,
6};
7use ic_certification::Label;
8use leb128::read;
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 replica rejected the message.
55    #[error("The replica returned a replica error: {0}")]
56    ReplicaError(RejectResponse),
57
58    /// The replica returned an HTTP error.
59    #[error("The replica returned an HTTP Error: {0}")]
60    HttpError(HttpErrorPayload),
61
62    /// The status endpoint returned an invalid status.
63    #[error("Status endpoint returned an invalid status.")]
64    InvalidReplicaStatus,
65
66    /// The call was marked done, but no reply was provided.
67    #[error("Call was marked as done but we never saw the reply. Request ID: {0}")]
68    RequestStatusDoneNoReply(String),
69
70    /// A string error occurred in an external tool.
71    #[error("A tool returned a string message error: {0}")]
72    MessageError(String),
73
74    /// There was an error reading a LEB128 value.
75    #[error("Error reading LEB128 value: {0}")]
76    Leb128ReadError(#[from] read::Error),
77
78    /// A string was invalid UTF-8.
79    #[error("Error in UTF-8 string: {0}")]
80    Utf8ReadError(#[from] Utf8Error),
81
82    /// The lookup path was absent in the certificate.
83    #[error("The lookup path ({0:?}) is absent in the certificate.")]
84    LookupPathAbsent(Vec<Label>),
85
86    /// The lookup path was unknown in the certificate.
87    #[error("The lookup path ({0:?}) is unknown in the certificate.")]
88    LookupPathUnknown(Vec<Label>),
89
90    /// The lookup path did not make sense for the certificate.
91    #[error("The lookup path ({0:?}) does not make sense for the certificate.")]
92    LookupPathError(Vec<Label>),
93
94    /// The request status at the requested path was invalid.
95    #[error("The request status ({1}) at path {0:?} is invalid.")]
96    InvalidRequestStatus(Vec<Label>, String),
97
98    /// The certificate verification failed.
99    #[error("Certificate verification failed.")]
100    CertificateVerificationFailed(),
101
102    /// The certificate contained a delegation that does not include the effective_canister_id in the canister_ranges field.
103    #[error("Certificate is not authorized to respond to queries for this canister. While developing: Did you forget to set effective_canister_id?")]
104    CertificateNotAuthorized(),
105
106    /// There was a length mismatch between the expected and actual length of the BLS DER-encoded public key.
107    #[error(
108        r#"BLS DER-encoded public key must be ${expected} bytes long, but is {actual} bytes long."#
109    )]
110    DerKeyLengthMismatch {
111        /// The expected length of the key.
112        expected: usize,
113        /// The actual length of the key.
114        actual: usize,
115    },
116
117    /// There was a mismatch between the expected and actual prefix of the BLS DER-encoded public key.
118    #[error("BLS DER-encoded public key is invalid. Expected the following prefix: ${expected:?}, but got ${actual:?}")]
119    DerPrefixMismatch {
120        /// The expected key prefix.
121        expected: Vec<u8>,
122        /// The actual key prefix.
123        actual: Vec<u8>,
124    },
125
126    /// The status response did not contain a root key.
127    #[error("The status response did not contain a root key.  Status: {0}")]
128    NoRootKeyInStatus(Status),
129
130    /// The invocation to the wallet call forward method failed with an error.
131    #[error("The invocation to the wallet call forward method failed with the error: {0}")]
132    WalletCallFailed(String),
133
134    /// The wallet operation failed.
135    #[error("The  wallet operation failed: {0}")]
136    WalletError(String),
137
138    /// The wallet canister must be upgraded. See [`dfx wallet upgrade`](https://smartcontracts.org/docs/developers-guide/cli-reference/dfx-wallet.html)
139    #[error("The wallet canister must be upgraded: {0}")]
140    WalletUpgradeRequired(String),
141
142    /// The transport was not specified in the [`AgentBuilder`](super::AgentBuilder).
143    #[error("Missing replica transport in the Agent Builder.")]
144    MissingReplicaTransport(),
145
146    /// The response size exceeded the provided limit.
147    #[error("Response size exceeded limit.")]
148    ResponseSizeExceededLimit(),
149
150    /// An unknown error occurred during communication with the replica.
151    #[error("An error happened during communication with the replica: {0}")]
152    TransportError(Box<dyn std::error::Error + Send + Sync>),
153
154    /// There was a mismatch between the expected and actual CBOR data during inspection.
155    #[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}")]
156    CallDataMismatch {
157        /// The field that was mismatched.
158        field: String,
159        /// The value that was expected to be in the CBOR.
160        value_arg: String,
161        /// The value that was actually in the CBOR.
162        value_cbor: String,
163    },
164}
165
166impl PartialEq for AgentError {
167    fn eq(&self, other: &Self) -> bool {
168        // Verify the debug string is the same. Some of the subtypes of this error
169        // don't implement Eq or PartialEq, so we cannot rely on derive.
170        format!("{:?}", self) == format!("{:?}", other)
171    }
172}
173
174impl Display for RejectResponse {
175    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
176        f.write_fmt(format_args!(
177            "Replica Error: reject code {:?}, reject message {}, error code {:?}",
178            self.reject_code, self.reject_message, self.error_code,
179        ))
180    }
181}
182
183/// A HTTP error from the replica.
184pub struct HttpErrorPayload {
185    /// The HTTP status code.
186    pub status: u16,
187    /// The MIME type of `content`.
188    pub content_type: Option<String>,
189    /// The body of the error.
190    pub content: Vec<u8>,
191}
192
193impl HttpErrorPayload {
194    fn fmt_human_readable(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
195        // No matter content_type is TEXT or not,
196        // always try to parse it as a String.
197        // When fail, print the raw byte array
198        f.write_fmt(format_args!(
199            "Http Error: status {}, content type {:?}, content: {}",
200            http::StatusCode::from_u16(self.status)
201                .map_or_else(|_| format!("{}", self.status), |code| format!("{}", code)),
202            self.content_type.clone().unwrap_or_default(),
203            String::from_utf8(self.content.clone()).unwrap_or_else(|_| format!(
204                "(unable to decode content as UTF-8: {:?})",
205                self.content
206            ))
207        ))?;
208        Ok(())
209    }
210}
211
212impl Debug for HttpErrorPayload {
213    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
214        self.fmt_human_readable(f)
215    }
216}
217
218impl Display for HttpErrorPayload {
219    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
220        self.fmt_human_readable(f)
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::HttpErrorPayload;
227    use crate::AgentError;
228
229    #[test]
230    fn content_type_none_valid_utf8() {
231        let payload = HttpErrorPayload {
232            status: 420,
233            content_type: None,
234            content: vec![104, 101, 108, 108, 111],
235        };
236
237        assert_eq!(
238            format!("{}", AgentError::HttpError(payload)),
239            r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "", content: hello"#,
240        );
241    }
242
243    #[test]
244    fn content_type_none_invalid_utf8() {
245        let payload = HttpErrorPayload {
246            status: 420,
247            content_type: None,
248            content: vec![195, 40],
249        };
250
251        assert_eq!(
252            format!("{}", AgentError::HttpError(payload)),
253            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])"#,
254        );
255    }
256
257    #[test]
258    fn formats_text_plain() {
259        let payload = HttpErrorPayload {
260            status: 420,
261            content_type: Some("text/plain".to_string()),
262            content: vec![104, 101, 108, 108, 111],
263        };
264
265        assert_eq!(
266            format!("{}", AgentError::HttpError(payload)),
267            r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "text/plain", content: hello"#,
268        );
269    }
270
271    #[test]
272    fn formats_text_plain_charset_utf8() {
273        let payload = HttpErrorPayload {
274            status: 420,
275            content_type: Some("text/plain; charset=utf-8".to_string()),
276            content: vec![104, 101, 108, 108, 111],
277        };
278
279        assert_eq!(
280            format!("{}", AgentError::HttpError(payload)),
281            r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "text/plain; charset=utf-8", content: hello"#,
282        );
283    }
284
285    #[test]
286    fn formats_text_html() {
287        let payload = HttpErrorPayload {
288            status: 420,
289            content_type: Some("text/html".to_string()),
290            content: vec![119, 111, 114, 108, 100],
291        };
292
293        assert_eq!(
294            format!("{}", AgentError::HttpError(payload)),
295            r#"The replica returned an HTTP Error: Http Error: status 420 <unknown status code>, content type "text/html", content: world"#,
296        );
297    }
298}