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(
28        "PocketIC requires an explicit server URL; provision/start it through ic_testkit::pic or supply a running server"
29    )]
30    MissingServerUrl,
31    #[error("PocketIC transport failed: {source}")]
32    Transport {
33        context: Box<RequestContext>,
34        #[source]
35        source: reqwest::Error,
36    },
37    #[error("PocketIC response body failed: {source}")]
38    Body {
39        context: Box<RequestContext>,
40        #[source]
41        source: reqwest::Error,
42    },
43    #[error("PocketIC response JSON failed: {source}")]
44    Json {
45        context: Box<RequestContext>,
46        #[source]
47        source: serde_json::Error,
48    },
49    #[error("PocketIC server returned {status}: {message}")]
50    Server {
51        context: Box<RequestContext>,
52        status: StatusCode,
53        message: String,
54    },
55    #[error("PocketIC request deadline expired")]
56    Timeout { context: Box<RequestContext> },
57    #[error("PocketIC response exceeds {limit} bytes")]
58    ResponseTooLarge {
59        context: Box<RequestContext>,
60        limit: usize,
61    },
62    #[error("canister rejected the call: {0:?}")]
63    Canister(crate::pocket_ic::RejectResponse),
64    #[error("Candid failed: {0}")]
65    Candid(#[from] candid::Error),
66    #[error("CBOR failed: {0}")]
67    Cbor(#[from] serde_cbor::Error),
68    #[error("I/O failed: {0}")]
69    Io(#[from] std::io::Error),
70    #[error("invalid PocketIC response: {0}")]
71    Protocol(String),
72}
73
74impl ClientError {
75    pub(crate) fn with_ingress(
76        mut self,
77        ingress: crate::pocket_ic::common::rest::RawMessageId,
78    ) -> Self {
79        match &mut self {
80            Self::Transport { context, .. }
81            | Self::Body { context, .. }
82            | Self::Json { context, .. }
83            | Self::Server { context, .. }
84            | Self::ResponseTooLarge { context, .. }
85            | Self::Timeout { context } => context.ingress = Some(ingress),
86            _ => {}
87        }
88        self
89    }
90
91    pub fn request_context(&self) -> Option<&RequestContext> {
92        match self {
93            Self::Transport { context, .. }
94            | Self::Body { context, .. }
95            | Self::Json { context, .. }
96            | Self::Server { context, .. }
97            | Self::ResponseTooLarge { context, .. }
98            | Self::Timeout { context } => Some(context),
99            _ => None,
100        }
101    }
102}
103
104impl From<crate::pocket_ic::RejectResponse> for ClientError {
105    fn from(value: crate::pocket_ic::RejectResponse) -> Self {
106        Self::Canister(value)
107    }
108}