1use super::{CandidCallContext, CandidCallError, PocketIcCallFailure, transport};
2use ic_platform::candid::{DecodeError, DecodeLimits, decode_args};
3use ic_platform::pocket_ic::{PocketIc, RejectResponse};
4use ic_platform::{
5 candid::contract::{ArgumentDecoder, ArgumentEncoder, encode_args},
6 principal::ExternalPrincipal as Principal,
7};
8
9#[derive(Clone, Copy)]
10struct CallContext<'a> {
11 operation: &'static str,
12 canister_id: Principal,
13 caller: Principal,
14 method: &'a str,
15}
16
17impl CallContext<'_> {
18 fn to_error_context(self) -> CandidCallContext {
19 CandidCallContext::new(self.operation, self.canister_id, self.caller, self.method)
20 }
21}
22
23pub fn update_candid_args<T, A>(
30 pic: &PocketIc,
31 canister_id: Principal,
32 caller: Principal,
33 method: &str,
34 args: A,
35 limits: DecodeLimits,
36) -> Result<T, CandidCallError>
37where
38 T: for<'de> ArgumentDecoder<'de>,
39 A: ArgumentEncoder,
40{
41 let context = CallContext {
42 operation: "update_call",
43 canister_id,
44 caller,
45 method,
46 };
47 call_candid(
48 context,
49 args,
50 |bytes| pic.update_call(canister_id, caller, method, bytes),
51 |bytes| decode_args(bytes, limits),
52 )
53}
54
55pub fn query_candid_args<T, A>(
62 pic: &PocketIc,
63 canister_id: Principal,
64 caller: Principal,
65 method: &str,
66 args: A,
67 limits: DecodeLimits,
68) -> Result<T, CandidCallError>
69where
70 T: for<'de> ArgumentDecoder<'de>,
71 A: ArgumentEncoder,
72{
73 let context = CallContext {
74 operation: "query_call",
75 canister_id,
76 caller,
77 method,
78 };
79 call_candid(
80 context,
81 args,
82 |bytes| pic.query_call(canister_id, caller, method, bytes),
83 |bytes| decode_args(bytes, limits),
84 )
85}
86
87fn call_candid<T, A, F, D>(
88 context: CallContext<'_>,
89 args: A,
90 call: F,
91 decode: D,
92) -> Result<T, CandidCallError>
93where
94 A: ArgumentEncoder,
95 F: FnOnce(Vec<u8>) -> Result<Vec<u8>, RejectResponse>,
96 D: FnOnce(&[u8]) -> Result<T, DecodeError>,
97{
98 let bytes = encode_args(args).map_err(|err| CandidCallError::Encode {
99 context: Box::new(context.to_error_context()),
100 message: err.to_string(),
101 })?;
102 let result = run_raw_call(context, || call(bytes))?;
103 decode(&result).map_err(|source| CandidCallError::Decode {
104 context: Box::new(context.to_error_context()),
105 bytes: result.len(),
106 source: Box::new(source),
107 })
108}
109
110fn run_raw_call<F>(context: CallContext<'_>, call: F) -> Result<Vec<u8>, CandidCallError>
111where
112 F: FnOnce() -> Result<Vec<u8>, RejectResponse>,
113{
114 match transport::call_or_resume(call) {
115 Ok(bytes) => Ok(bytes),
116 Err(PocketIcCallFailure::Rejected(response)) => Err(CandidCallError::CanisterReject {
117 context: Box::new(context.to_error_context()),
118 response: Box::new(response),
119 }),
120 Err(PocketIcCallFailure::Operation(source)) => Err(CandidCallError::Transport {
121 context: Box::new(context.to_error_context()),
122 source,
123 }),
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use ic_platform::candid::{DecodeError, DecodeLimits, decode_args};
130 use ic_platform::principal::ExternalPrincipal as Principal;
131
132 use super::{CallContext, CandidCallError, call_candid, run_raw_call};
133
134 const LIMITS: DecodeLimits = DecodeLimits {
135 reply_bytes: 65_536,
136 decoding_quota: 1_000_000,
137 skipping_quota: 100_000,
138 type_entries: 1024,
139 header_bytes: 8192,
140 };
141
142 #[test]
143 fn decode_error_includes_call_context() {
144 let context = CallContext {
145 operation: "query_call",
146 canister_id: Principal::anonymous(),
147 caller: Principal::management_canister(),
148 method: "get",
149 };
150
151 let err = call_candid(
152 context,
153 (),
154 |_| Ok(vec![0xde, 0xad]),
155 |bytes| decode_args::<(u64,)>(bytes, LIMITS),
156 )
157 .expect_err("decode fails");
158
159 assert!(err.to_string().contains("candid decode failed"));
160 assert!(err.to_string().contains("operation=query_call"));
161 assert!(err.to_string().contains("method=get"));
162 assert!(err.to_string().contains("bytes=2"));
163 assert!(std::error::Error::source(&err).unwrap().is::<DecodeError>());
164 let CandidCallError::Decode {
165 context,
166 bytes,
167 source,
168 } = err
169 else {
170 panic!("expected decode failure");
171 };
172 assert_eq!(bytes, 2);
173 assert_eq!(context.method, "get");
174 assert!(matches!(*source, DecodeError::Candid(_)));
175 }
176
177 #[test]
178 fn caller_decode_limits_retain_typed_refusals() {
179 let context = CallContext {
180 operation: "query_call",
181 canister_id: Principal::anonymous(),
182 caller: Principal::anonymous(),
183 method: "get",
184 };
185 let bytes = ic_platform::candid::contract::encode_one(42_u64).unwrap();
186 let length = bytes.len();
187 let error = call_candid(
188 context,
189 (),
190 |_| Ok(bytes),
191 |bytes| {
192 decode_args::<(u64,)>(
193 bytes,
194 DecodeLimits {
195 reply_bytes: 0,
196 ..LIMITS
197 },
198 )
199 },
200 )
201 .unwrap_err();
202 let cause = std::error::Error::source(&error)
203 .unwrap()
204 .downcast_ref::<DecodeError>()
205 .unwrap();
206 assert!(
207 matches!(cause, DecodeError::ReplyTooLarge { actual, maximum: 0 } if *actual == length)
208 );
209
210 let bytes = ic_platform::candid::contract::encode_args((vec![(); 100_000],)).unwrap();
213 let error = call_candid(
214 context,
215 (),
216 |_| Ok(bytes),
217 |bytes| {
218 decode_args::<(Vec<()>,)>(
219 bytes,
220 DecodeLimits {
221 decoding_quota: 0,
222 ..LIMITS
223 },
224 )
225 },
226 )
227 .unwrap_err();
228 let cause = std::error::Error::source(&error)
229 .unwrap()
230 .downcast_ref::<DecodeError>()
231 .unwrap();
232 assert!(matches!(cause, DecodeError::Candid(_)));
233 assert!(std::error::Error::source(cause).is_some());
234 }
235
236 #[test]
237 fn reply_arguments_preserve_top_level_arity_and_wide_values() {
238 let context = CallContext {
239 operation: "update_call",
240 canister_id: Principal::anonymous(),
241 caller: Principal::management_canister(),
242 method: "measure",
243 };
244 let values = (u128::MAX, u128::from(u64::MAX) + 1);
245 let bytes = ic_platform::candid::contract::encode_args(values).unwrap();
246 assert_eq!(
247 ic_platform::candid::decode_args::<(u128, u128)>(&bytes, LIMITS).unwrap(),
248 values
249 );
250 assert!(ic_platform::candid::decode_one::<(u128, u128)>(&bytes, LIMITS).is_err());
251 let tuple = ic_platform::candid::contract::encode_one(values).unwrap();
252 assert!(ic_platform::candid::decode_args::<(u128, u128)>(&tuple, LIMITS).is_err());
253 assert_eq!(
254 ic_platform::candid::decode_one::<(u128, u128)>(&tuple, LIMITS).unwrap(),
255 values
256 );
257 let no_replies = ic_platform::candid::contract::encode_args(()).unwrap();
258 ic_platform::candid::decode_args::<()>(&no_replies, LIMITS).unwrap();
259 let one_reply = ic_platform::candid::contract::encode_args((7_u64,)).unwrap();
260 assert_eq!(
261 ic_platform::candid::decode_args::<(u64,)>(&one_reply, LIMITS).unwrap(),
262 (7,)
263 );
264 let result: (u64, bool, String) = call_candid(
265 context,
266 (42_u64, false),
267 |args| {
268 assert_eq!(
269 ic_platform::candid::decode_args::<(u64, bool)>(&args, LIMITS).unwrap(),
270 (42, false)
271 );
272 Ok(ic_platform::candid::contract::encode_args((9_u64, true, "ready")).unwrap())
273 },
274 |bytes| decode_args(bytes, LIMITS),
275 )
276 .unwrap();
277 assert_eq!(result, (9, true, "ready".into()));
278 }
279
280 #[test]
281 fn invalid_reply_arguments_keep_decode_context() {
282 let context = CallContext {
283 operation: "query_call",
284 canister_id: Principal::anonymous(),
285 caller: Principal::management_canister(),
286 method: "measure",
287 };
288 for bytes in [
289 vec![0xde, 0xad],
290 ic_platform::candid::contract::encode_args((7_u64,)).unwrap(),
291 ] {
292 let length = bytes.len();
293 let err = call_candid(
294 context,
295 (),
296 |_| Ok(bytes),
297 |bytes| decode_args::<(u64, bool)>(bytes, LIMITS),
298 )
299 .unwrap_err();
300 assert!(err.to_string().contains(&format!("bytes={length}")));
301 let CandidCallError::Decode {
302 context: observed,
303 bytes,
304 ..
305 } = err
306 else {
307 panic!("expected decode failure");
308 };
309 assert_eq!(bytes, length);
310 assert_eq!(observed.operation, "query_call");
311 assert_eq!(observed.canister_id, context.canister_id);
312 assert_eq!(observed.caller, context.caller);
313 assert_eq!(observed.method, "measure");
314 }
315 }
316
317 #[test]
318 fn dead_instance_panic_is_classified_as_transport() {
319 let context = CallContext {
320 operation: "query_call",
321 canister_id: Principal::anonymous(),
322 caller: Principal::management_canister(),
323 method: "get",
324 };
325 let error = run_raw_call(context, || -> Result<Vec<u8>, ic_platform::pocket_ic::RejectResponse> {
326 panic!("HTTP failure: reqwest::Error {{ kind: Request, url: \"http://127.0.0.1:1234/instances/0/update/execute_ingress_message\", source: ConnectError(\"tcp connect error\", Os {{ code: 111, kind: ConnectionRefused, message: \"Connection refused\" }}) }}");
327 })
328 .unwrap_err();
329
330 let source = std::error::Error::source(&error)
331 .unwrap()
332 .downcast_ref::<super::super::PocketIcOperationError>()
333 .unwrap();
334 assert!(source.is_transport());
335 assert!(source.message().starts_with("HTTP failure: "));
336 assert!(error.to_string().contains("operation=query_call"));
337 assert!(super::super::is_dead_pocket_ic_transport_error(&error));
338 let CandidCallError::Transport { source: owned, .. } = error else {
339 panic!("expected transport failure");
340 };
341 assert!(owned.is_transport());
342 }
343
344 #[test]
345 fn unrelated_panics_resume_with_the_original_payload() {
346 for message in [
347 "application worker channel closed",
348 "fixture expected ConnectionRefused but observed another result",
349 ] {
350 let context = CallContext {
351 operation: "query_call",
352 canister_id: Principal::anonymous(),
353 caller: Principal::management_canister(),
354 method: "get",
355 };
356 let payload = message.to_owned();
357 let original_allocation = payload.as_ptr() as usize;
358 let result = std::panic::catch_unwind(|| {
359 run_raw_call(
360 context,
361 || -> Result<Vec<u8>, ic_platform::pocket_ic::RejectResponse> {
362 std::panic::panic_any(payload);
363 },
364 )
365 });
366 let resumed = result
367 .expect_err("application panic must resume")
368 .downcast::<String>()
369 .expect("original panic type must be preserved");
370 assert_eq!(resumed.as_str(), message);
371 assert_eq!(resumed.as_ptr() as usize, original_allocation);
372 }
373 let payload = Box::new(7_u8);
374 let original = std::ptr::from_ref(payload.as_ref());
375 let result = std::panic::catch_unwind(|| {
376 run_raw_call(
377 CallContext {
378 operation: "query_call",
379 canister_id: Principal::anonymous(),
380 caller: Principal::anonymous(),
381 method: "get",
382 },
383 || -> Result<Vec<u8>, ic_platform::pocket_ic::RejectResponse> {
384 std::panic::panic_any(payload);
385 },
386 )
387 });
388 let resumed = result.unwrap_err().downcast::<Box<u8>>().unwrap();
389 assert_eq!(std::ptr::from_ref(resumed.as_ref().as_ref()), original);
390 }
391}