Skip to content

candid: a rejected query's reject message is replaced by "Do not know how to serialize a BigInt" #712

Description

@b3hr4d

What happens

CandidAdapter.fetchFromTmpHack and CandidAdapter.compileRemote report a query that got no reply with

throw new Error(`Query failed: ${JSON.stringify(response)}`)

(packages/candid/src/adapter.ts:381 and :429 on origin/main c39bf67).

For a rejected query, HttpAgent.query returns the replica's node signatures along with the reject: signatures: [{ timestamp, signature, identity }]. The agent decodes each timestamp (a CBOR u64, in nanoseconds) as a bigint. JSON.stringify throws TypeError: Do not know how to serialize a BigInt on it, so every rejected query ends in that TypeError and the reject code and message are lost.

This affects every rejection, not an edge case. The interface spec gives every query response at least one node signature (signatures ([+ node-signature])), and the agent refuses a response without one when it verifies query signatures, which is its default.

The most visible case is a canister with no Candid: no candid:service metadata and no __get_candid_interface_tmp_hack method. The replica rejects the fallback query with Error from Canister <id>: Canister has no query method '__get_candid_interface_tmp_hack'. (reject code 5, IC0536). getCandidDefinition() and CandidReactor.initialize() then say:

Failed to retrieve Candid source by any method: the candid:service metadata was not available; calling __get_candid_interface_tmp_hack failed: Do not know how to serialize a BigInt

#541 added each attempt's cause to this message so that "no Candid" could be told apart from a network or root-key failure. On this path, the cause it shows is the TypeError.

compileRemote loses the didjs canister's reject the same way. Take a local replica with no didjs canister: the reject is Canister bd3sg-teaaa-aaaaa-qaaba-cai not found (reject code 3, IC0301). parseCandidSource without the local parser then reports Do not know how to serialize a BigInt.

Repro (measured on origin/main c39bf67)

Agent level, plain Node with @icp-sdk/core 6.1.0. A fetch answers /api/v3/canister/<id>/query with the CBOR body a replica sends for a reject. agent.query(...) resolves to an object with the keys requestDetails, status, reject_code, reject_message, error_code, signatures, httpDetails, requestId. typeof response.signatures[0].timestamp is "bigint", and JSON.stringify(response) throws TypeError: Do not know how to serialize a BigInt.

Adapter level: the real HttpAgent, with query signature verification on, against a fetch that answers like a replica. The body is { status: "rejected", reject_code, reject_message, error_code, signatures: [{ timestamp: <u64 ns>, signature, identity }] }, carrying an Ed25519 node signature over the reject. The node's key is put in the agent's subnetNodeKeyExpirableStore.

const adapter = new CandidAdapter({
  clientManager: { agent, isLocal: false, subscribe: () => () => {} },
})
await adapter.fetchFromTmpHack("ryjl3-tyaaa-aaaaa-aaaba-cai")
// origin/main: Error: Do not know how to serialize a BigInt

The existing unit tests stub agent.query with { reject_code, reject_message }, which has no signatures. The stubbed object serializes without error, so those tests never saw the throw.

Why it matters

The reject message is the only thing that says why the Candid couldn't be fetched or compiled: a missing method, a missing canister, a stopped canister, a canister out of cycles. The candid adapter docs ("Error Handling") tell users to pass the "Failed to retrieve Candid source" message on, because "the rest of the message says what stopped each attempt". On this path it doesn't.

Fix plan

Build the error from the reject fields instead of serializing the response:

Query failed (reject code 5, error code IC0536): Error from Canister ryjl3-tyaaa-aaaaa-aaaba-cai: Canister has no query method '__get_candid_interface_tmp_hack'.
  • Keep the Query failed prefix, since existing tests and user code may match on it.
  • Apply the change to both fetchFromTmpHack and compileRemote.
  • Put the reject message last, because a replica message can end with a line of help and a link.
  • Leave out the error code when the replica sends none; the spec calls it optional.

Alternative considered: reuse core's processQueryCallResponse and throw the SDK's RejectError. Direct callers of these public methods would get a different error class, and the SDK's multi-line message with the request ID. The mocked responses in adapter.test.ts would also need a status. Not chosen.

Acceptance

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions