Description
GET /v1/blocks/head/header intermittently fails with:
{"error":"Failed to get client at block: Cannot construct OnlineClientAtBlock: cannot find the block header for block <hash>"}
The handler resolves the head to a hash and fetches its header in two separate RPC calls. Between them the tip can reorg, or the second call can hit a lagging node (behind a load-balanced RPC endpoint) that hasn't imported the block yet. The node correctly returns "not found" (subxt BlockHeaderNotFound) and the request fails. Transient, self-recovers in ~1s, recurs under load.
Where
crates/server/src/handlers/blocks/get_blocks_head_header.rs
finalized=true (default) path: at_current_block() (line 174); the race is inside subxt. This is the reported case.
finalized=false path: explicit two calls chain_get_block_hash(None) then at_block(best_hash) (lines 178-187).
handle_use_rc_block: same pattern (lines 240-248).
Fix
- Collapse
finalized=false to a single chain_get_header(None) and compute the hash locally via utils::hash::compute_block_hash_from_header_json. The RC handler already does this (rc/blocks/get_head_header.rs:119).
- Retry once on
BlockHeaderNotFound around at_current_block() / at_block(), this is what fixes the default path. Variant already classified in utils/block.rs:71.
Belongs in the API, not subxt: subxt correctly reports the node's response; the retry policy is application-specific.
Description
GET /v1/blocks/head/headerintermittently fails with:{"error":"Failed to get client at block: Cannot construct OnlineClientAtBlock: cannot find the block header for block <hash>"}The handler resolves the head to a hash and fetches its header in two separate RPC calls. Between them the tip can reorg, or the second call can hit a lagging node (behind a load-balanced RPC endpoint) that hasn't imported the block yet. The node correctly returns "not found" (subxt
BlockHeaderNotFound) and the request fails. Transient, self-recovers in ~1s, recurs under load.Where
crates/server/src/handlers/blocks/get_blocks_head_header.rsfinalized=true(default) path:at_current_block()(line 174); the race is inside subxt. This is the reported case.finalized=falsepath: explicit two callschain_get_block_hash(None)thenat_block(best_hash)(lines 178-187).handle_use_rc_block: same pattern (lines 240-248).Fix
finalized=falseto a singlechain_get_header(None)and compute the hash locally viautils::hash::compute_block_hash_from_header_json. The RC handler already does this (rc/blocks/get_head_header.rs:119).BlockHeaderNotFoundaroundat_current_block()/at_block(), this is what fixes the default path. Variant already classified inutils/block.rs:71.Belongs in the API, not subxt: subxt correctly reports the node's response; the retry policy is application-specific.