Skip to main content

ic_testkit/pocket_ic/
error.rs

1//! Errors retain the request evidence needed for consumer-owned recovery.
2
3use reqwest::StatusCode;
4
5/// A computation accepted by the PocketIC server.
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct Operation {
8    pub state_label: String,
9    pub op_id: String,
10}
11
12/// Evidence about a failed HTTP exchange, not permission to retry it.
13#[derive(Clone, Debug)]
14pub struct RequestContext {
15    pub url: String,
16    pub operation: Option<Operation>,
17    /// An ingress message already accepted before this failure.
18    pub ingress: Option<crate::pocket_ic::common::rest::RawMessageId>,
19    /// The request may have reached the server. A transport failure is not rejection.
20    pub may_have_executed: bool,
21    pub status: Option<StatusCode>,
22}
23
24/// Failures from the maintained client. Sources are preserved without string matching.
25#[derive(Debug, thiserror::Error)]
26pub enum ClientError {
27    #[error("PocketIC transport failed: {source}")]
28    Transport {
29        context: Box<RequestContext>,
30        #[source]
31        source: reqwest::Error,
32    },
33    #[error("PocketIC response body failed: {source}")]
34    Body {
35        context: Box<RequestContext>,
36        #[source]
37        source: reqwest::Error,
38    },
39    #[error("PocketIC response JSON failed: {source}")]
40    Json {
41        context: Box<RequestContext>,
42        #[source]
43        source: serde_json::Error,
44    },
45    #[error("PocketIC server returned {status}: {message}")]
46    Server {
47        context: Box<RequestContext>,
48        status: StatusCode,
49        message: String,
50    },
51    #[error("PocketIC request deadline expired")]
52    Timeout { context: Box<RequestContext> },
53    #[error("PocketIC response exceeds {limit} bytes")]
54    ResponseTooLarge {
55        context: Box<RequestContext>,
56        limit: usize,
57    },
58    #[error("canister rejected the call: {0:?}")]
59    Canister(crate::pocket_ic::RejectResponse),
60    #[error("Candid failed: {0}")]
61    Candid(#[from] candid::Error),
62    #[error("CBOR failed: {0}")]
63    Cbor(#[from] serde_cbor::Error),
64    #[error("I/O failed: {0}")]
65    Io(#[from] std::io::Error),
66    #[error("invalid PocketIC response: {0}")]
67    Protocol(String),
68}
69
70impl ClientError {
71    pub(crate) fn with_ingress(
72        mut self,
73        ingress: crate::pocket_ic::common::rest::RawMessageId,
74    ) -> Self {
75        match &mut self {
76            Self::Transport { context, .. }
77            | Self::Body { context, .. }
78            | Self::Json { context, .. }
79            | Self::Server { context, .. }
80            | Self::ResponseTooLarge { context, .. }
81            | Self::Timeout { context } => context.ingress = Some(ingress),
82            _ => {}
83        }
84        self
85    }
86
87    pub fn request_context(&self) -> Option<&RequestContext> {
88        match self {
89            Self::Transport { context, .. }
90            | Self::Body { context, .. }
91            | Self::Json { context, .. }
92            | Self::Server { context, .. }
93            | Self::ResponseTooLarge { context, .. }
94            | Self::Timeout { context } => Some(context),
95            _ => None,
96        }
97    }
98}
99
100impl From<crate::pocket_ic::RejectResponse> for ClientError {
101    fn from(value: crate::pocket_ic::RejectResponse) -> Self {
102        Self::Canister(value)
103    }
104}