Repository navigation
Return the Ledger API's error from websocket calls (0.10.1) - #62
Conversation
The participant answers a rejected request with one message that
carries code and cause, then closes the stream. The websocket
functions read that message as data: get returned an empty list,
get_with_callback passed it to the callback, and update::subscribe
passed it to the handler. Each then reported success.
A shared check in websocket/mod.rs now turns such a message into
Err("Ledger API returned <code>: <cause>"). The tests run each
function against a local websocket server that sends the message a
Canton 3.6.1 participant sent on devnet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The error handling is consistent across all three affected functions and is covered by focused success and failure tests.
Review effort: Balanced
Findings: None
What changed in this PR
Fixes WebSocket clients so Ledger API rejection messages propagate as errors instead of successful empty results.
Changes:
- Detects Ledger API error messages before processing stream data.
- Adds local WebSocket tests for error and success paths.
- Releases workspace version 0.10.1 with changelog updates.
| File | Description |
|---|---|
crates/ledger/src/websocket/mod.rs |
Adds error parsing and tests. |
crates/ledger/src/websocket/active_contracts.rs |
Propagates API errors from contract streams. |
crates/ledger/src/websocket/update.rs |
Propagates API errors from update streams. |
CHANGELOG.md |
Documents the fix. |
Cargo.toml |
Bumps the workspace version. |
Cargo.lock |
Updates workspace package versions. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Copilot review loop: disposition summary
By category: there are no security, functionality or cosmetic findings. Outcome: the loop converged in round 1. Copilot recommends approval and left no inline comments. No items are open. |
canton-lib 0.10.1 returns the Ledger API's error when a participant rejects a query. 0.10.0 and earlier returned an empty result, so a rejected query read as "no contracts". Releasing cbtc-lib 0.9.0 on 0.10.1 gives consumers the 3.6.1 request format and this fix in one bump. All seven pins move together, and Cargo.lock keeps one entry for each canton-lib crate. Refs DLC-link/canton-lib#62 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rollout recommendation above is replacedThe "Rollout recommendation" section names cbtc-lib What each service must do
Why Cargo treats each git tag as a separate source. A service that pins canton-lib The other points of the recommendation still apply. v0.11.0 includes the fix from this PR, and DevNet services go first. |
What this PR does
Old behavior
The Ledger API answers a request it rejects with one error message, then closes the stream. The three websocket functions read that message as data and reported success:
websocket::active_contracts::getreturnedOk(vec![]).websocket::active_contracts::get_with_callbackpassed the error to the callback and returnedOk(()).websocket::update::subscribepassed the error to the handler and returnedOk(())when the stream closed.A caller therefore saw "no contracts", and nothing in the logs showed an error.
How the bug became visible now. The bug runs only when the participant refuses a request. When the participant answers normally, canton-lib works correctly.
filterfield. canton-lib 0.9.0 uses that format in every request. So the participant refuses every request, and the faulty code runs on every call. The result is "balance 0" for a party that has holdings.How old the bug is. The three faulty lines date from the first commit,
6046f99on 2025-12-02.git log -Sfound no later commit that adds or removes them. So every release fromv0.1.0tov0.10.0has the bug. Two questions stay open:New behavior
Each function returns
Err("Ledger API returned <code>: <cause>"). The callback and the handler get no error message. The signatures do not change.Verification
filterfield. The participant answeredDEPRECATED_API_DISABLED, and the query reported 0 holdings. The same query on canton-lib 0.10.0 returned 4 holdings, 20.1751930825 CBTC.crates/ledger/src/websocket/mod.rs. Each runs a function against a local websocket server. The server sends either the error JSON that the devnet participant sent, or ordinary stream messages.getreturnedOk([]). The 3 control tests passed, which shows that the fake server works.cargo test --workspace --all-targetspasses on rustc 1.94.1: 214 passed, 0 failed, 33 ignored.cargo clippy --workspace --all-targets -- -D warningsandcargo fmt --checkare clean.The change that enables it
ledger_api_errorinwebsocket/mod.rsparses a message into a privateLedgerApiError { code, cause }. If the parse succeeds, the message is an error.Errwhen it matches.Caveats
get_with_callbackorsubscribethat read the error text in its callback now getsErrinstead. I found no such caller in this repository.codeandcausefields. Ordinary stream messages have neither field.Rollout recommendation
Release this as
v0.10.1. The public API does not change, so this is a patch release.The bump to 3.6.1 and this fix should reach the services in one rollout:
v0.10.1.v0.10.0tov0.10.1, then rerun the devnet balance check. That PR stays a draft until this tag exists. It releases cbtc-libv0.9.0.v0.10.1, notv0.10.0. canton-lib Send eventFormat and updateFormat to the Ledger API (0.10.0) #61 names four of them: attestor-admin, omnibus, pqs-status-page and the cbtc-faucet backend.v0.9.0. If a service also pins canton-lib, move that pin tov0.10.1in the same change.Move both pins together. Cargo treats each git tag as a separate source, and semver does not merge them. A service with canton-lib
v0.10.0beside cbtc-libv0.9.0builds twocommonpackages, and its code stops compiling.Bump DevNet services first. The DevNet participants already run 3.6.1. A service there that still uses canton-lib 0.9.0 now reads empty results without an error. The later upgrades are TestNet on 2026-10-28 and MainNet on 2026-11-07.
Details
Why a private struct and not
JsCantonErrorcanton_api_client::models::JsCantonErrordescribes the same message, but it also requirescontextanderrorCategory. If the participant left out either field, the parse would fail, and the error would read as data again. So the check requires onlycodeandcause. A comment besideledger_api_errorrecords this.The captured message
The test server
serve_oncebinds127.0.0.1:0and accepts one connection. It reads the request, sends the given messages, and closes. It namesdaml.ws.authin the handshake, because the client offers that subprotocol. The handshake closure allowsclippy::result_large_err, because theCallbacktrait of tungstenite fixes the closure's error type.The existing check stays
The check for "A security-sensitive error has been received" stays unchanged in both active-contracts functions.
Commits
Follows #61 · Blocks DLC-link/cbtc-lib#94
🤖 Generated with Claude Code