Skip to content

Commit 65ab571

Browse files
authored
Merge pull request #20 from tempoxyz/fix-get-logs-max-results-retry
fix: retry getLogs with narrower range on max results exceeded
2 parents 842be9c + 1089324 commit 65ab571

File tree

4 files changed

+96
-1
lines changed

4 files changed

+96
-1
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/rpc-tester/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ alloy-primitives.workspace = true
1212
alloy-rpc-types.workspace = true
1313
alloy-rpc-types-trace.workspace = true
1414
alloy-provider = { workspace = true, features = ["trace-api", "debug-api"] }
15+
alloy-json-rpc = "0.11.1"
16+
alloy-transport = "0.11.1"
1517

1618
assert-json-diff.workspace = true
1719
eyre.workspace = true

crates/rpc-tester/src/get_logs.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//! Helper for `eth_getLogs` with automatic retry on "max results exceeded" errors.
2+
3+
use alloy_primitives::BlockNumber;
4+
use alloy_provider::{network::AnyNetwork, Provider};
5+
use alloy_rpc_types::{Filter, Log};
6+
7+
/// The result type returned by `get_logs`.
8+
pub type GetLogsResult<T> =
9+
Result<T, alloy_json_rpc::RpcError<alloy_transport::TransportErrorKind>>;
10+
11+
/// Fetches logs with automatic retry when the RPC returns a "max results exceeded" error.
12+
///
13+
/// Some RPC providers limit the number of logs returned in a single request. When exceeded,
14+
/// they return an error like:
15+
/// `"query exceeds max results 20000, retry with the range 24383075-24383096"`
16+
///
17+
/// This function parses such errors and retries with the suggested narrower block range.
18+
pub async fn get_logs_with_retry<P: Provider<AnyNetwork>>(
19+
provider: &P,
20+
filter: &Filter,
21+
) -> GetLogsResult<Vec<Log>> {
22+
match provider.get_logs(filter).await {
23+
Ok(logs) => Ok(logs),
24+
Err(e) => {
25+
if let Some((from, to)) = parse_max_results_error(&e) {
26+
let narrowed_filter = filter.clone().from_block(from).to_block(to);
27+
provider.get_logs(&narrowed_filter).await
28+
} else {
29+
Err(e)
30+
}
31+
}
32+
}
33+
}
34+
35+
/// Parses an error to extract the suggested block range from "max results exceeded" errors.
36+
///
37+
/// Expected format: "query exceeds max results N, retry with the range FROM-TO"
38+
fn parse_max_results_error<E: std::fmt::Display>(error: &E) -> Option<(BlockNumber, BlockNumber)> {
39+
let msg = error.to_string();
40+
41+
if !msg.contains("max results") {
42+
return None;
43+
}
44+
45+
// Look for pattern like "range 24383075-24383096"
46+
let range_prefix = "range ";
47+
let range_start = msg.find(range_prefix)?;
48+
let range_part = &msg[range_start + range_prefix.len()..];
49+
50+
// Parse "FROM-TO" (stop at first non-numeric, non-dash char)
51+
let range_end =
52+
range_part.find(|c: char| !c.is_ascii_digit() && c != '-').unwrap_or(range_part.len());
53+
let range_str = &range_part[..range_end];
54+
55+
let mut parts = range_str.split('-');
56+
let from: BlockNumber = parts.next()?.parse().ok()?;
57+
let to: BlockNumber = parts.next()?.parse().ok()?;
58+
59+
Some((from, to))
60+
}
61+
62+
#[cfg(test)]
63+
mod tests {
64+
use super::*;
65+
66+
#[test]
67+
fn test_parse_max_results_error_message() {
68+
let error_msg = "query exceeds max results 20000, retry with the range 24383075-24383096";
69+
let result = parse_max_results_error(&error_msg);
70+
assert_eq!(result, Some((24383075, 24383096)));
71+
}
72+
73+
#[test]
74+
fn test_parse_non_matching_error() {
75+
let error_msg = "some other error";
76+
let result = parse_max_results_error(&error_msg);
77+
assert_eq!(result, None);
78+
}
79+
80+
#[test]
81+
fn test_parse_with_trailing_text() {
82+
let error_msg = "query exceeds max results 20000, retry with the range 100-200, extra info";
83+
let result = parse_max_results_error(&error_msg);
84+
assert_eq!(result, Some((100, 200)));
85+
}
86+
}

crates/rpc-tester/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! rpc-tester library
22
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
33

4+
#[doc(hidden)]
5+
pub mod get_logs;
46
mod tester;
57
pub use tester::RpcTester;
68
mod report;
@@ -49,6 +51,9 @@ macro_rules! rpc_with_block {
4951
}
5052

5153
/// Macro to call the `get_logs` rpc method and box the future result.
54+
///
55+
/// Uses [`get_logs::get_logs_with_retry`] to automatically handle "max results exceeded" errors
56+
/// by retrying with the narrower block range suggested in the error message.
5257
#[macro_export]
5358
macro_rules! get_logs {
5459
($self:expr, $arg:expr) => {{
@@ -58,7 +63,7 @@ macro_rules! get_logs {
5863
$self
5964
.test_rpc_call(stringify!(get_logs), args_str, move |provider: &P| {
6065
let filter = filter.clone();
61-
async move { provider.get_logs(&filter).await }
66+
async move { $crate::get_logs::get_logs_with_retry(provider, &filter).await }
6267
})
6368
.await
6469
}) as Pin<Box<dyn Future<Output = (MethodName, Result<(), TestError>)> + Send>>

0 commit comments

Comments
 (0)