@@ -6,7 +6,7 @@ use alloy_transport_http::Http;
66use raiko_lib:: clear_line;
77use reqwest_alloy:: Client ;
88use reth_primitives:: revm_primitives:: { AccountInfo , Bytecode } ;
9- use std:: collections:: HashMap ;
9+ use std:: { collections:: HashMap , env , time :: Duration } ;
1010use tracing:: debug;
1111
1212use crate :: {
@@ -15,6 +15,36 @@ use crate::{
1515 MerkleProof ,
1616} ;
1717
18+ /// Env: total per-request timeout for L1/L2 JSON-RPC (connect + send + response body).
19+ /// Default 300s so large `eth_getProof` / batch calls can finish on slow nodes.
20+ const ENV_RPC_HTTP_TIMEOUT_SECS : & str = "RAIKO_RPC_HTTP_TIMEOUT_SECS" ;
21+ /// Env: TCP connect timeout only. Default 30s.
22+ const ENV_RPC_HTTP_CONNECT_TIMEOUT_SECS : & str = "RAIKO_RPC_HTTP_CONNECT_TIMEOUT_SECS" ;
23+
24+ fn build_rpc_reqwest_client ( ) -> RaikoResult < Client > {
25+ let timeout_secs: u64 = env:: var ( ENV_RPC_HTTP_TIMEOUT_SECS )
26+ . ok ( )
27+ . and_then ( |s| s. parse ( ) . ok ( ) )
28+ . unwrap_or ( 300 ) ;
29+ let connect_secs: u64 = env:: var ( ENV_RPC_HTTP_CONNECT_TIMEOUT_SECS )
30+ . ok ( )
31+ . and_then ( |s| s. parse ( ) . ok ( ) )
32+ . unwrap_or ( 30 ) ;
33+
34+ Client :: builder ( )
35+ . timeout ( Duration :: from_secs ( timeout_secs) )
36+ . connect_timeout ( Duration :: from_secs ( connect_secs) )
37+ . build ( )
38+ . map_err ( |e| RaikoError :: RPC ( format ! ( "Failed to build RPC HTTP client: {e}" ) ) )
39+ }
40+
41+ fn rpc_http_transport ( url : reqwest:: Url ) -> RaikoResult < ( Http < Client > , bool ) > {
42+ let client = build_rpc_reqwest_client ( ) ?;
43+ let http = Http :: with_client ( client, url) ;
44+ let is_local = http. guess_local ( ) ;
45+ Ok ( ( http, is_local) )
46+ }
47+
1848#[ derive( Clone ) ]
1949pub struct RpcBlockDataProvider {
2050 pub provider : ReqwestProvider ,
@@ -31,9 +61,11 @@ impl RpcBlockDataProvider {
3161 "provider rpc url: {:?} for block_number {}" ,
3262 url, block_number
3363 ) ;
64+ let ( http, is_local) = rpc_http_transport ( url) ?;
65+ let rpc_client = RpcClient :: new ( http. clone ( ) , is_local) ;
3466 Ok ( Self {
35- provider : ProviderBuilder :: new ( ) . on_provider ( RootProvider :: new_http ( url . clone ( ) ) ) ,
36- client : ClientBuilder :: default ( ) . http ( url ) ,
67+ provider : ProviderBuilder :: new ( ) . on_provider ( RootProvider :: new ( rpc_client ) ) ,
68+ client : ClientBuilder :: default ( ) . transport ( http , is_local ) ,
3769 block_numbers : vec ! [ block_number, block_number + 1 ] ,
3870 } )
3971 }
@@ -49,9 +81,11 @@ impl RpcBlockDataProvider {
4981 "Batch provider rpc: {:?} for block_number {}" ,
5082 url, block_numbers[ 0 ]
5183 ) ;
84+ let ( http, is_local) = rpc_http_transport ( url) ?;
85+ let rpc_client = RpcClient :: new ( http. clone ( ) , is_local) ;
5286 Ok ( Self {
53- provider : ProviderBuilder :: new ( ) . on_provider ( RootProvider :: new_http ( url . clone ( ) ) ) ,
54- client : ClientBuilder :: default ( ) . http ( url ) ,
87+ provider : ProviderBuilder :: new ( ) . on_provider ( RootProvider :: new ( rpc_client ) ) ,
88+ client : ClientBuilder :: default ( ) . transport ( http , is_local ) ,
5589 block_numbers,
5690 } )
5791 }
@@ -368,3 +402,61 @@ impl BlockDataProvider for RpcBlockDataProvider {
368402 Ok ( storage_proofs)
369403 }
370404}
405+
406+ #[ cfg( test) ]
407+ mod tests {
408+ use super :: * ;
409+ use serial_test:: serial;
410+ use tokio:: io:: AsyncReadExt ;
411+ use tokio:: net:: TcpListener ;
412+
413+ fn set_short_rpc_timeouts_for_test ( ) {
414+ std:: env:: set_var ( ENV_RPC_HTTP_TIMEOUT_SECS , "1" ) ;
415+ std:: env:: set_var ( ENV_RPC_HTTP_CONNECT_TIMEOUT_SECS , "1" ) ;
416+ }
417+
418+ fn clear_rpc_timeout_env ( ) {
419+ std:: env:: remove_var ( ENV_RPC_HTTP_TIMEOUT_SECS ) ;
420+ std:: env:: remove_var ( ENV_RPC_HTTP_CONNECT_TIMEOUT_SECS ) ;
421+ }
422+
423+ /// Local TCP server: read the JSON-RPC POST then stall so the client hits `reqwest` timeout.
424+ async fn spawn_stall_after_accept_json_rpc ( ) -> String {
425+ let listener = TcpListener :: bind ( "127.0.0.1:0" ) . await . expect ( "bind test listener" ) ;
426+ let addr = listener. local_addr ( ) . expect ( "listener addr" ) ;
427+ tokio:: spawn ( async move {
428+ if let Ok ( ( mut stream, _) ) = listener. accept ( ) . await {
429+ let mut buf = [ 0u8 ; 8192 ] ;
430+ let _ = stream. read ( & mut buf) . await ;
431+ tokio:: time:: sleep ( Duration :: from_secs ( 600 ) ) . await ;
432+ }
433+ } ) ;
434+ format ! ( "http://{addr}" )
435+ }
436+
437+ #[ tokio:: test]
438+ #[ serial]
439+ async fn get_blocks_returns_rpc_error_when_http_times_out ( ) {
440+ set_short_rpc_timeouts_for_test ( ) ;
441+ let url = spawn_stall_after_accept_json_rpc ( ) . await ;
442+ let provider = RpcBlockDataProvider :: new ( & url, 1 )
443+ . await
444+ . expect ( "provider new with short timeout" ) ;
445+ let result = provider. get_blocks ( & [ ( 1 , false ) ] ) . await ;
446+ clear_rpc_timeout_env ( ) ;
447+
448+ let err = result. expect_err ( "expected RPC failure when server does not respond in time" ) ;
449+ let RaikoError :: RPC ( payload) = & err else {
450+ panic ! ( "expected RaikoError::RPC, got {err:?}" ) ;
451+ } ;
452+ let lower = payload. to_lowercase ( ) ;
453+ // Reqwest may report `operation timed out` or a generic `error sending request for url (...)`
454+ // when the overall request timeout fires.
455+ assert ! (
456+ lower. contains( "timeout" )
457+ || lower. contains( "timed out" )
458+ || payload. contains( "error sending request for url" ) ,
459+ "expected timeout or stalled-request error, got: {err}"
460+ ) ;
461+ }
462+ }
0 commit comments