Skip to content

Return the Ledger API's error from websocket calls (0.10.1) - #62

Merged
gyorgybalazsi merged 2 commits into
mainfrom
fix/websocket-server-errors
Oct 7, 2026
Merged

gyorgybalazsi merged 2 commits into
mainfrom
fix/websocket-server-errors

Conversation

@gyorgybalazsi

@gyorgybalazsi gyorgybalazsi commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

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::get returned Ok(vec![]).
  • websocket::active_contracts::get_with_callback passed the error to the callback and returned Ok(()).
  • websocket::update::subscribe passed the error to the handler and returned Ok(()) 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.

  • Before Canton 3.6.1, the participant almost never refused. canton-lib's requests were valid. A refusal came only from an unusual request, such as one with a missing permission. So the faulty code almost never ran, and nobody saw the bug.
  • Canton 3.6.1 no longer accepts the old request format, the filter field. 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.
  • canton-lib 0.10.0 sends the new format, so the participant accepts its requests again, and the bug is hidden again. The faulty code is still there. The next refusal, for any reason, will again look like "no contracts".

How old the bug is. The three faulty lines date from the first commit, 6046f99 on 2025-12-02. git log -S found no later commit that adds or removes them. So every release from v0.1.0 to v0.10.0 has the bug. Two questions stay open:

  • I don't know whether the bug ever hid a real error in production. An empty result looks the same as "no contracts", so no log shows it.
  • The first commit adds the whole crate at once. The code may come from an earlier repository, which would make the bug older than 2025-12-02. I have not checked that.

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

  • I measured the bug on devnet. All devnet participants run Splice 0.9.0, which ships Canton 3.6.1. I queried one party with cbtc-lib on canton-lib 0.9.0, which sends the disabled filter field. The participant answered DEPRECATED_API_DISABLED, and the query reported 0 holdings. The same query on canton-lib 0.10.0 returned 4 holdings, 20.1751930825 CBTC.
  • I wrote 6 new tests in 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.
  • Before the fix, the 3 error tests failed with the bug's symptom. For example, get returned Ok([]). The 3 control tests passed, which shows that the fake server works.
  • After the fix, cargo test --workspace --all-targets passes on rustc 1.94.1: 214 passed, 0 failed, 33 ignored.
  • cargo clippy --workspace --all-targets -- -D warnings and cargo fmt --check are clean.
  • I did not run the ignored live tests. I did not run the fixed code against a participant that rejects a request. The fixed code sends a valid request, so devnet returns no error to test against. The test fixture is the participant's real message.

The change that enables it

  • ledger_api_error in websocket/mod.rs parses a message into a private LedgerApiError { code, cause }. If the parse succeeds, the message is an error.
  • The three functions call it on each text message, before any other parsing, and stop with Err when it matches.

Caveats

  • A caller of get_with_callback or subscribe that read the error text in its callback now gets Err instead. I found no such caller in this repository.
  • The check matches any top-level JSON object that has string code and cause fields. 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:

  1. Merge this PR, then tag and publish v0.10.1.
  2. Move the cbtc-lib pins in Pin canton-lib v0.11.0 for Canton 3.6.1 and release 0.9.0 cbtc-lib#94 from v0.10.0 to v0.10.1, then rerun the devnet balance check. That PR stays a draft until this tag exists. It releases cbtc-lib v0.9.0.
  3. Bump the services that use canton-lib directly straight to v0.10.1, not v0.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.
  4. Bump the services that use cbtc-lib to v0.9.0. If a service also pins canton-lib, move that pin to v0.10.1 in 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.0 beside cbtc-lib v0.9.0 builds two common packages, 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 JsCantonError
canton_api_client::models::JsCantonError describes the same message, but it also requires context and errorCategory. If the participant left out either field, the parse would fail, and the error would read as data again. So the check requires only code and cause. A comment beside ledger_api_error records this.

The captured message

{"code":"DEPRECATED_API_DISABLED","cause":"The fields filter/verbose of the /v2/state/active-contracts requests was deprecated in Canton 3.4, is disabled by default since Canton 3.6 and will be removed in Canton 3.7. Set `canton.participants.<participant>.features.deprecated.enable-deprecated-parameters-34 = true` to re-enable it temporarily.","correlationId":null,"traceId":"…"}

The test server
serve_once binds 127.0.0.1:0 and accepts one connection. It reads the request, sends the given messages, and closes. It names daml.ws.auth in the handshake, because the client offers that subprotocol. The handshake closure allows clippy::result_large_err, because the Callback trait 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

  1. Return the Ledger API's error from websocket calls.
  2. Release 0.10.1.

Follows #61 · Blocks DLC-link/cbtc-lib#94

🤖 Generated with Claude Code

gyorgybalazsi and others added 2 commits October 7, 2026 15:30
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@gyorgybalazsi

Copy link
Copy Markdown
Contributor Author

Copilot review loop: disposition summary

Round Commits Findings Fixed Fixed w/ mods Won't-fix Filed issue
1 f5a90bd…9e25146 0 0 0 0 0
Total 0 0 0 0 0

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.

@gyorgybalazsi
gyorgybalazsi requested a review from scolear October 7, 2026 13:49
@gyorgybalazsi
gyorgybalazsi merged commit 81ea9e7 into main Oct 7, 2026
4 checks passed
gyorgybalazsi added a commit to DLC-link/cbtc-lib that referenced this pull request Oct 7, 2026
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>
@gyorgybalazsi

Copy link
Copy Markdown
Contributor Author

The rollout recommendation above is replaced

The "Rollout recommendation" section names v0.10.1. Use v0.11.0 instead. canton-lib released v0.11.0 on 2026-10-08 in #64, after this PR merged.

cbtc-lib v0.9.0 now pins canton-lib v0.11.0 and canton-api-client 3.6.1-0.1.0. See DLC-link/cbtc-lib#94.

What each service must do

  1. Move a direct canton-lib pin to v0.11.0, not v0.10.1. This applies to attestor-admin, omnibus, pqs-status-page and the cbtc-faucet backend.
  2. Move a cbtc-lib pin to v0.9.0. If the service also pins canton-lib, move that pin to v0.11.0 in the same change.
  3. Move a direct canton-api-client dependency to 3.6.1-0.1.0. ledger re-exports the client's models, so two client versions make the same model types differ, and the build fails.
  4. Check every match on the client's Kind variants. Client 3.6.1 adds CanActAsAnyParty as KindOneOf1, so each later variant number moves up by one. For example, CanReadAs moves from KindOneOf3 to KindOneOf4.

Why v0.10.1 no longer works with cbtc-lib

Cargo treats each git tag as a separate source. A service that pins canton-lib v0.10.1 next to cbtc-lib v0.9.0 builds two common packages, and its code stops compiling.

The other points of the recommendation still apply. v0.11.0 includes the fix from this PR, and DevNet services go first.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants