Error (with temporary fix): Using ic-agent in cloudflare workers

This happens when using ic-agent and deploying to cloudflare worker. I am not sure about the root cause for the error, but I have temporarily fixed it with custom code shared below. Sharing here to highlight the issue and a temporary workaround.

ic-agent = { version = "0.33.0", features = ["wasm-bindgen", "pem"] }

On doing

    let request_id = agent
        .update(&canister_id, "call_name")
        .with_arg(candid::encode_args(()).unwrap())
        .call_and_wait()
        .await
        .unwrap()

Get below error. (using panic::set_hook(Box::new(console_error_panic_hook::hook)); for the trace)


  global window unavailable

  Stack:

  Error
      at ht (file:///Users/komalsai/learning/cf-exp/reclaim_canisters/build/worker/shim.mjs:2:6168)
      at [object Object]xfd569
      at [object Object]x12dbe2
      at [object Object]x130e5a
      at [object Object]x139097
      at [object Object]x136757
      at [object Object]x1370db
      at [object Object]x13524f
      at [object Object]x128e61
      at [object Object]x138218


✘ [ERROR] panicked at /Users/komalsai/.cargo/registry/src/index.crates.io-6f17d22bba15001f/ic-agent-0.33.0/src/agent/mod.rs:783:22:

  unable to setTimeout: JsValue(RuntimeError: unreachable
  RuntimeError: unreachable
      at [object Object]x1390dc
      at [object Object]x12dc0b
      at [object Object]x130e5a
      at [object Object]x139097
      at [object Object]x136757
      at [object Object]x1370db
      at [object Object]x13524f
      at [object Object]x128e61
      at [object Object]x138218
      at K (file:///Users/komalsai/learning/cf-exp/reclaim_canisters/build/worker/shim.mjs:2:428))

  Stack:

  Error
      at ht (file:///Users/komalsai/learning/cf-exp/reclaim_canisters/build/worker/shim.mjs:2:6168)
      at [object Object]xfd569
      at [object Object]x12dbe2
      at [object Object]x130e5a
      at [object Object]x139097
      at [object Object]x136757
      at [object Object]x1370db
      at [object Object]x132849
      at [object Object]x3419e
      at [object Object]xbdcc7


  A hanging Promise was canceled. This happens when the worker runtime is waiting for a Promise from
  JavaScript to resolve, but has detected that the Promise cannot possibly ever resolve because all
  code and events related to the Promise's I/O context have already finished.


✘ [ERROR] Uncaught (in response) Error: The script will never generate a response.

After some digging, it is due to this part of code in ic-agent

    pub async fn wait(
        &self,
        request_id: RequestId,
        effective_canister_id: Principal,
    ) -> Result<Vec<u8>, AgentError> {
        let mut retry_policy = Self::get_retry_policy();

        let mut request_accepted = false;
        loop {
            match self.poll(&request_id, effective_canister_id).await? {
                PollResult::Submitted => {}
                PollResult::Accepted => {
                    if !request_accepted {
                        // The system will return RequestStatusResponse::Unknown
                        // (PollResult::Submitted) until the request is accepted
                        // and we generally cannot know how long that will take.
                        // State transitions between Received and Processing may be
                        // instantaneous. Therefore, once we know the request is accepted,
                        // we should restart the backoff so the request does not time out.

                        retry_policy.reset();
                        request_accepted = true;
                    }
                }
                PollResult::Completed(result) => return Ok(result),
            };

            match retry_policy.next_backoff() {
                #[cfg(not(target_family = "wasm"))]
                Some(duration) => tokio::time::sleep(duration).await,
                #[cfg(all(target_family = "wasm", feature = "wasm-bindgen"))]
                Some(duration) => {
                    wasm_bindgen_futures::JsFuture::from(js_sys::Promise::new(&mut |rs, rj| {
                        if let Err(e) = web_sys::window()
                            .expect("global window unavailable")
                            .set_timeout_with_callback_and_timeout_and_arguments_0(
                                &rs,
                                duration.as_millis() as _,
                            )
                        {
                            use wasm_bindgen::UnwrapThrowExt;
                            rj.call1(&rj, &e).unwrap_throw();
                        }
                    }))
                    .await
                    .expect("unable to setTimeout");
                }
                None => return Err(AgentError::TimeoutWaitingForResponse()),
            }
        }
    }

Issue is with set_timeout_with_callback_and_timeout_and_arguments_0 . This approach is for async sleep timeout (shared here - Async sleep in Rust/wasm32? - #6 by zeroexcuses - The Rust Programming Language Forum). Another approach shared in the same post is to use gloo_timers which fixed the issue for me.

My code looks like this now . Essentially had to tweak the wait functionality using gloo-timers.

// Usage instead of call_and_wait()

    let request_id = agent
        .update(&canister_id, "save_snapshot_json")
        .with_arg(candid::encode_args(()).unwrap())
        .call()
        .await
        .unwrap();

    let res = custom_agent_wait(&agent, request_id, canister_id)
        .await
        .unwrap();

-------------------

use gloo_timers::future::TimeoutFuture


fn get_retry_policy() -> ExponentialBackoff<SystemClock> {
    ExponentialBackoffBuilder::new()
        .with_initial_interval(Duration::from_millis(500))
        .with_max_interval(Duration::from_secs(1))
        .with_multiplier(1.4)
        .with_max_elapsed_time(Some(Duration::from_secs(60 * 5)))
        .build()
}

pub async fn custom_agent_wait(
    agent: &Agent,
    request_id: RequestId,
    effective_canister_id: Principal,
) -> Result<Vec<u8>> {
    let mut retry_policy = get_retry_policy();
    let mut request_accepted = false;

    loop {
        match agent
            .poll(&request_id, effective_canister_id)
            .await
            .unwrap()
        {
            PollResult::Submitted => {}
            PollResult::Accepted => {
                if !request_accepted {
                    request_accepted = true;
                }
            }
            PollResult::Completed(result) => {
                return Ok(result);
            }
        };

        match retry_policy.next_backoff() {
            Some(duration) => {
                let duration_in_millis = duration.clone().as_millis();
                spawn_local(async move {
                    TimeoutFuture::new(duration_in_millis as u32).await;
                    web_sys::console::log_1(&"Hello from a timeout!".into());
                });
            }
            None => return Err("Timeout".into()),
        }
    }
}


1 Like