Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ The format is based on [Keep a Changelog].

[Keep a Changelog]: http://keepachangelog.com/en/1.0.0/

## [Unreleased]

### [Added]

- feat(http-client): expose connect timeout option ([#1643](https://github.com/paritytech/jsonrpsee/pull/1643))

## [v0.26.0] - 2025-08-11

This is just a small release; the only breaking change is the addition of `max_frame_size` to `WsTransportClientBuilder`, which necessitates a minor version bump.
Expand Down
17 changes: 17 additions & 0 deletions client/http-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pub struct HttpClientBuilder<HttpMiddleware = Identity, RpcMiddleware = Logger>
max_request_size: u32,
max_response_size: u32,
request_timeout: Duration,
connect_timeout: Option<Duration>,
#[cfg(feature = "tls")]
certificate_store: CertificateStore,
id_kind: IdKind,
Expand Down Expand Up @@ -114,6 +115,17 @@ impl<HttpMiddleware, RpcMiddleware> HttpClientBuilder<HttpMiddleware, RpcMiddlew
self
}

/// Set a timeout for establishing the TCP connection (default is none).
///
/// This bounds only the TCP connection establishment and does not include DNS
/// resolution or the TLS handshake; the overall request is still bounded by
/// [`Self::request_timeout`]. If the host resolves to multiple addresses, the
/// timeout is divided evenly across them.
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = Some(timeout);
self
}

/// Set the maximum number of concurrent requests. Default disabled.
pub fn max_concurrent_requests(mut self, max_concurrent_requests: usize) -> Self {
self.max_concurrent_requests = Some(max_concurrent_requests);
Expand Down Expand Up @@ -248,6 +260,7 @@ impl<HttpMiddleware, RpcMiddleware> HttpClientBuilder<HttpMiddleware, RpcMiddlew
keep_alive_duration: self.keep_alive_duration,
keep_alive_interval: self.keep_alive_interval,
keep_alive_retries: self.keep_alive_retries,
connect_timeout: self.connect_timeout,
}
}

Expand All @@ -271,6 +284,7 @@ impl<HttpMiddleware, RpcMiddleware> HttpClientBuilder<HttpMiddleware, RpcMiddlew
keep_alive_duration: self.keep_alive_duration,
keep_alive_retries: self.keep_alive_retries,
keep_alive_interval: self.keep_alive_interval,
connect_timeout: self.connect_timeout,
}
}
}
Expand All @@ -291,6 +305,7 @@ where
max_request_size,
max_response_size,
request_timeout,
connect_timeout,
#[cfg(feature = "tls")]
certificate_store,
id_kind,
Expand All @@ -313,6 +328,7 @@ where
keep_alive_duration,
keep_alive_interval,
keep_alive_retries,
connect_timeout,
#[cfg(feature = "tls")]
certificate_store,
}
Expand All @@ -338,6 +354,7 @@ impl Default for HttpClientBuilder {
max_request_size: TEN_MB_SIZE_BYTES,
max_response_size: TEN_MB_SIZE_BYTES,
request_timeout: Duration::from_secs(60),
connect_timeout: None,
#[cfg(feature = "tls")]
certificate_store: CertificateStore::Native,
id_kind: IdKind::Number,
Expand Down
27 changes: 27 additions & 0 deletions client/http-client/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

use std::time::Duration;

use crate::HttpClientBuilder;
use crate::types::error::{ErrorCode, ErrorObject};
use jsonrpsee_core::ClientError;
Expand Down Expand Up @@ -51,6 +53,31 @@ async fn method_call_works() {
assert_eq!("hello", &result);
}

#[tokio::test]
async fn connect_timeout_fires_before_request_timeout() {
// 192.0.2.1 is in TEST-NET-1 (RFC 5737): reserved and not routable, so the TCP handshake never
// completes. With a short connect timeout the request must fail quickly instead of waiting out the
// much longer request timeout — exactly what would regress if `connect_timeout` were not wired
// through to the connector.
let client = HttpClientBuilder::default()
.connect_timeout(Duration::from_millis(100))
.request_timeout(Duration::from_secs(60))
.build("http://192.0.2.1:9")
.unwrap();

// Bound the test well under the 60s request timeout: the connect should fail in ~100ms. If it
// regressed and hung until the request timeout, this outer 10s bound trips and fails the test.
let res = tokio::time::timeout(Duration::from_secs(10), client.request::<String, _>("say_hello", rpc_params![]))
.await
.expect("connect should fail well before the request timeout");

match res {
Ok(_) => panic!("expected a connect error, got a successful response"),
Err(ClientError::RequestTimeout) => panic!("request failed via the request timeout, not the connect timeout"),
Err(_) => {}
}
}

#[tokio::test]
async fn method_call_with_wrong_id_kind() {
let exp = "id as string";
Expand Down
13 changes: 13 additions & 0 deletions client/http-client/src/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ pub struct HttpTransportClientBuilder<L> {
pub(crate) keep_alive_interval: Option<std::time::Duration>,
/// KEEP_ALIVE retries
pub(crate) keep_alive_retries: Option<u32>,
/// TCP connect timeout
pub(crate) connect_timeout: Option<std::time::Duration>,
}

impl Default for HttpTransportClientBuilder<Identity> {
Expand All @@ -126,6 +128,7 @@ impl HttpTransportClientBuilder<Identity> {
keep_alive_duration: None,
keep_alive_interval: None,
keep_alive_retries: None,
connect_timeout: None,
}
}
}
Expand Down Expand Up @@ -172,6 +175,12 @@ impl<L> HttpTransportClientBuilder<L> {
self
}

/// Configure the TCP connect timeout for the connection.
pub fn set_connect_timeout(mut self, timeout: Option<std::time::Duration>) -> Self {
self.connect_timeout = timeout;
self
}

/// Configure the keep-alive interval for the connection.
pub fn set_keep_alive_interval(mut self, interval: Option<std::time::Duration>) -> Self {
self.keep_alive_interval = interval;
Expand All @@ -197,6 +206,7 @@ impl<L> HttpTransportClientBuilder<L> {
keep_alive_duration: self.keep_alive_duration,
keep_alive_retries: self.keep_alive_retries,
keep_alive_interval: self.keep_alive_interval,
connect_timeout: self.connect_timeout,
}
}

Expand All @@ -220,6 +230,7 @@ impl<L> HttpTransportClientBuilder<L> {
keep_alive_duration,
keep_alive_interval,
keep_alive_retries,
connect_timeout,
} = self;
let mut url = Url::parse(target.as_ref()).map_err(|e| Error::Url(format!("Invalid URL: {e}")))?;

Expand All @@ -235,6 +246,7 @@ impl<L> HttpTransportClientBuilder<L> {
connector.set_keepalive(keep_alive_duration);
connector.set_keepalive_interval(keep_alive_interval);
connector.set_keepalive_retries(keep_alive_retries);
connector.set_connect_timeout(connect_timeout);
HttpBackend::Http(Client::builder(TokioExecutor::new()).build(connector))
}
#[cfg(feature = "tls")]
Expand All @@ -251,6 +263,7 @@ impl<L> HttpTransportClientBuilder<L> {
http_conn.set_keepalive(keep_alive_duration);
http_conn.set_keepalive_interval(keep_alive_interval);
http_conn.set_keepalive_retries(keep_alive_retries);
http_conn.set_connect_timeout(connect_timeout);

let https_conn = match certificate_store {
CertificateStore::Native => {
Expand Down