fix(realtime): filtered fallback when a full-block eth_getLogs exceeds viem's body cap (issue #23)#144
Merged
Merged
Conversation
…s viem's body cap (issue #23) In RPC-transport realtime, ponder's block-data fetch (`fetchBlockEventData`) issues an UNFILTERED `eth_getLogs` pinned by `blockHash` — all logs in the block. On a "monster" block the response body exceeds viem's hardcoded ~10 MiB `readResponseBody` cap (viem >=2.31) and viem throws `ResponseBodyTooLargeError`. Because the request is pinned by `blockHash`, every retry deterministically hits the same oversized body: the chain stalls then crash-loops at the tip, surfacing as an `unhandledRejection` after N futile retries (exit 75). No wrong data is ever stored — a restart backfills past the block with filtered fetches — so this is availability-not-correctness, inherited from upstream ponder. Both Portal paths are structurally immune (backfill fetches FILTERED logs; PORTAL_REALTIME= stream never issues a full-block log fetch); this is purely for RPC-realtime. Fix (issue disposition candidates 1 + 3), as a wiring-patch hunk across all four supported versions (0.15.17 / 0.16.6 / 0.16.7 / 0.16.8): - rpc/actions.ts: three exported, unit-testable helpers — `isResponseBodyTooLargeError` (matches by `error.name`, walking the `.cause` chain, since the class is absent from older pinned viem and the core floats `viem: ">=2"`); `buildFilteredBlockLogRequests` (deduped `{address, topic0}` set covering every registered log filter + factory — a factory-address filter contributes a wildcard address, re-narrowed downstream); and `eth_getFilteredBlockLogs` (one bounded filtered `eth_getLogs` per request, assembled + deduped by (blockHash, logIndex) + sorted by logIndex). - sync-realtime/index.ts: catch the oversized error at the block-data log fetch and fall back to the filtered assembly for the same block. The block fetch is decoupled from the logs fetch so an oversized logs leg never discards a healthy block. The empty-vs-non-empty-`logsBloom` check in `validateLogsAndBlock` is skipped ONLY on the empty-fallback path (the filtered result is legitimately empty on a monster block that matches no registered filter) — mirroring the `if (logs.length > 0)` guard historical filtered sync already uses; all per-log consistency checks still run when the fallback returns logs. - rpc/index.ts: `shouldRetry` returns `false` for `ResponseBodyTooLargeError`, so the deterministic oversized body surfaces immediately instead of after 9 futile retries or as an unhandledRejection. Regression test portal/realtime-getlogs-fallback.test.ts (mutation-verified RED on unpatched upstream): the request-building, fetch/assemble/dedup/sort, the non-retryable behavior, and the empty-fallback validation hazard the guard sidesteps. Both-version `sync-upstream.sh {0.16.8,0.16.6} --test` green (326 tests each); biome clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 10, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #23.
The bug (RPC-transport realtime only, inherited upstream)
In RPC-transport realtime, ponder's block-data fetch (
fetchBlockEventDatainsync-realtime/index.ts) issues an unfilteredeth_getLogspinned byblockHash— every log in the block. On a "monster" block the response body exceeds viem's hardcoded ~10 MiBreadResponseBodycap (viem ≥ 2.31) and viem throwsResponseBodyTooLargeError. Because the request is pinned byblockHash, every retry deterministically hits the same oversized body — the chain stalls, then crash-loops at the tip, surfacing as anunhandledRejectionafter N futile retries (exit 75). No wrong data is ever stored (a restart backfills past the block with filtered fetches), so this is availability-not-correctness, severity medium.Both Portal paths are structurally immune — historical backfill fetches filtered
eth_getLogs, andPORTAL_REALTIME=streamnever issues a full-block log fetch — so this fix is purely for users on RPC-transport realtime.The fix (issue disposition candidates 1 + 3)
A wiring-patch hunk applied to all four supported versions (
0.15.17/0.16.6/0.16.7/0.16.8):1. Filtered fallback (
rpc/actions.ts, three exported/unit-testable helpers)isResponseBodyTooLargeError(error)— matches byerror.name === "ResponseBodyTooLargeError", walking the.causechain. Deliberately notinstanceof: the class is absent from older pinned viem, and the ponder core floatsviem: ">=2", so a name test is robust across both. (Confirmed against the installed viem: 2.30.1 has no such class; a fresh>=2install resolves 2.55.0 which throws it raw at the call site.)buildFilteredBlockLogRequests({logFilters, factories})— the deduped{address, topic0}set covering every registered log filter and factory. A factory-address filter contributes a wildcard address (its child addresses are only discovered from this block's own logs, so they can't be pre-filtered) and is re-narrowed downstream; each factory contributes{factory.address, factory.eventSelector}.topic1..3and child-address matching are omitted because the caller re-applies exact matching on the assembled logs — so this only over-fetches (bounded, identical to what historical sync fetches), never under-fetches.eth_getFilteredBlockLogs(rpc, blockHash, {logFilters, factories})— one bounded filteredeth_getLogsper request, assembled and deduped by(blockHash, logIndex), sorted bylogIndex.Interception point (
sync-realtime/index.ts) — the unfiltered log fetch is wrapped; on the oversized error it falls back to the filtered assembly for the same block. The block fetch is decoupled from the logs fetch so an oversized logs leg never discards a healthy block. The empty-vs-non-empty-logsBloomcheck invalidateLogsAndBlockis skipped only on the empty-fallback path (the filtered result is legitimately empty on a monster block that matches no registered filter) — mirroring theif (logs.length > 0)guard that historical filtered sync already uses; all per-log consistency checks still run when the fallback returns logs.2. Non-retryable (
rpc/index.ts) —shouldRetryreturnsfalseforResponseBodyTooLargeError, so the deterministic oversized body surfaces immediately instead of after 9 futile retries or as anunhandledRejection.Known limitation (documented, not a regression)
The filtered fallback is bounded by the registered filters — a strict improvement over today's unconditional crash. A wildcard-address filter on a very hot
topic0(e.g.Transfer) could in principle still exceed the cap in the filtered request; that residual re-throw is now non-retryable and surfaces cleanly rather than crash-looping. As the issue notes, any fixed limit just moves the threshold.Tests & evidence
Regression test
portal/realtime-getlogs-fallback.test.ts— mutation-verified RED on unpatched upstream (the three helpers don't exist andshouldRetryis neither exported nor returnsfalse; withshouldRetrymerely exported it returnstruefor the oversized error, i.e. the bug). Covers: name/cause-chain matching, non-retryable behavior, request-building (static/factory/dedup/union), fetch→assemble→dedup→sort, and the empty-fallback validation hazard the guard sidesteps.scripts/sync-upstream.sh {0.16.8,0.16.6} --testgreen — 17 test files, 326 tests each (13 new).npx @biomejs/biome@2.5.2 check .clean (0 errors).Reported upstream-worthy: this is inherited upstream ponder realtime behavior.
🤖 Generated with Claude Code