Skip to content

feat: Add Client::call_async - #59

Draft
ValuedMammal wants to merge 2 commits into
bitcoindevkit:masterfrom
ValuedMammal:feat/call_async
Draft

feat: Add Client::call_async#59
ValuedMammal wants to merge 2 commits into
bitcoindevkit:masterfrom
ValuedMammal:feat/call_async

Conversation

@ValuedMammal

@ValuedMammal ValuedMammal commented May 14, 2026

Copy link
Copy Markdown
Contributor

Description

Adds Client::call_async — an async counterpart to Client::call for use with async transports.

The method accepts a send_fn: AsyncFn(Value) -> Result<Response, E> closure rather than passing the Request directly to the transport. Internally, call_async builds the request, serializes the request to an owned serde_json::Value, calls the send_fn, and deserializes the response like the sync path.

A working example is included in call_async.rs using bitreq::send_async() from bitreq's async feature.

let send_fn = |value: serde_json::Value| {
    let auth_header = auth_header.clone();
    async move {
        bitreq::post(URL)
            .with_header("Authorization", auth_header)
            .with_json(&value)?
            .send_async()
            .await?
            .json::<jsonrpc::Response>()
    }
};

let get_blockchain_info = client
    .call_async::<v29::GetBlockchainInfo, bitreq::Error, _>(&method, &[], send_fn)
    .await?
    .into_model()?;

Notes to the reviewers

Why AsyncFn(Value) -> Result<Response, E> instead of passing Request<'_> to the transport?

The natural design would mirror the sync API: F: for<'r> AsyncFn(Request<'r>) -> Result<Response, E>. This was explored but seems to hit a Rust language limitation. An async closure async |req| transport.send_request(req).await that captures a reference cannot satisfy for<'r> AsyncFn(Request<'r>), as the closure ties the future's lifetime to both the transport reference and the request lifetime, so the bound is never satisfied. Passing an owned Value breaks the lifetime chain entirely.

The tradeoff is that callers cannot use BitreqHttpTransport::send_request directly (its return type is tied to the borrow of self via a BoxFuture). The bitreq::post() free function shown in the example is a workaround that avoids that issue. Another alternative is to support the async flavor of Transport alongside the current bitreq client module, but this would lead to duplicating the API which is precisely what the sans-io design is meant to avoid.

Changelog notice

Added

  • Client::call_async: async RPC method accepting AsyncFn(Value) -> Result<Response, E> for use with async transports

Comment thread src/client.rs Outdated
Ok(response.result()?)
}

/// Execute the RPC asynchronously.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think it'll be great to expand the documentation for this fn too. Thank you

@tvpeter tvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this.

The implementation is sound and good to go.

I left a nit.

Comment thread Cargo.toml
Comment on lines +25 to +26
bitreq = { version = "0.3.7", features = ["async"] }
tokio = { version = "1", features = ["full"] }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than pull in these dev-dependencies right now, we could add Client::call_async in a separate commit and leave the async example as a draft.

(dev) deps: Add `bitreq` 0.3.7 with `async` feature
(dev) deps: Add `tokio` v1 with `full` feature
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants