pub struct SentRequest<T> { /* private fields */ }Expand description
Represents a pending response of type R from an outgoing request.
Returned by ConnectionTo::send_request, this type provides explicit response-consumption
modes. The API is intentionally designed to make it difficult to accidentally wait for a
response inside the dispatch loop.
§Anti-Footgun Design
You cannot directly .await a SentRequest. Instead, you must choose how to handle
the response:
§Option 1: Register an Ordered Callback (Safe in Handlers)
Calling on_receiving_result registers the callback and returns
immediately. When ordered consumption is selected before the response is routed during its
original dispatch, the loop waits for the callback to complete before processing the next
message:
cx.send_request(MyRequest {})
.on_receiving_result(async |result| {
match result {
Ok(response) => {
// Handle successful response
Ok(())
}
Err(error) => {
// Handle error
Err(error)
}
}
})?;§Option 2: Wait Outside the Dispatch Loop
Use block_task only when the current task already runs outside the
dispatch loop—for example, in the foreground future passed to connect_with or in a task
created with ConnectionTo::spawn. Never await it in a handler:
// ✅ Safe: Spawned task runs concurrently
cx.spawn({
let cx = cx.clone();
async move {
let response = cx.send_request(MyRequest {})
.block_task()
.await?;
// Process response...
Ok(())
}
})?;// ❌ NEVER do this in a handler - blocks the event loop!
connection.on_receive_request(async |req: MyRequest, responder, cx| {
let response = cx.send_request(MyRequest {})
.block_task() // This will deadlock!
.await?;
responder.respond(response)
}, agent_client_protocol::on_receive_request!())§Why This Design?
If you block the event loop while waiting for a response, the connection cannot process the incoming response message, creating a deadlock. This API design prevents that footgun by making blocking explicit and encouraging non-blocking patterns.
§Drop Behavior
By default, dropping a SentRequest before the SDK has received the
response sends a $/cancel_request notification asking the peer to cancel
the request, then discards the response when it arrives. Requests whose
eventual response should be ignored, but which should keep running on the
peer, should use detach instead.
§Incoming Transport EOF
If the incoming transport reaches clean EOF before the response arrives, every
consumption mode receives an error with the message Incoming transport closed and data containing
{"reason":"incoming_transport_closed","method":"..."}. Requests made
after incoming EOF fail immediately with the same error. Use
is_incoming_transport_closed to identify it.
Implementations§
Source§impl<T> SentRequest<T>
impl<T> SentRequest<T>
Sourcepub fn detach(self)
pub fn detach(self)
Detach this request handle without waiting for its response.
The response will be discarded when it arrives. This also disarms the drop-time automatic cancellation described in Drop Behavior, so use it for requests whose eventual response should be ignored, but which should keep running on the peer. The peer is still expected to answer the JSON-RPC request eventually; use a notification instead when no response is expected at all.
To ask the peer to stop the request, call cancel instead, or drop the
handle while automatic cancellation is armed.
Sourcepub fn cancel(&self) -> Result<(), Error>
pub fn cancel(&self) -> Result<(), Error>
Send a $/cancel_request notification for this outgoing request.
This uses the same peer and message wrapping that were used to send the
original request, so it is the preferred way to cancel a SentRequest
when the request handle is still available.
At most one $/cancel_request is ever sent per request: the first
cancel call sends it (and also prevents the drop-time automatic
cancellation described in Drop Behavior), while
later calls return Ok(()) without sending anything. Likewise, once
the SDK has routed the response to this handle, cancel becomes a
no-op: there is nothing left to cancel.
Errors are only reported by the call that attempts to send the notification.
Sourcepub fn forward_cancellation_from(self, source: RequestCancellation) -> Self
pub fn forward_cancellation_from(self, source: RequestCancellation) -> Self
Forward cancellation of another request to this one.
When the request that source belongs to is cancelled by its peer,
a $/cancel_request for this request is sent to its peer, using the
same wrapping as the original request. The response is still awaited
and delivered as usual (normal data or a cancellation error), so this
composes with block_task and
on_receiving_result.
This is the building block for proxies that forward a request with
custom logic instead of forward_response_to
(which wires this up automatically from its responder). Without it,
custom forwarding absorbs cancellation: the upstream marker is still
set, but nothing is sent downstream.
backend
.send_request(request)
.forward_cancellation_from(responder.cancellation())
.on_receiving_result(async move |result| {
// Custom result handling, e.g. bookkeeping or rewriting.
responder.respond_with_result(result)
})?;May be called multiple times; cancellation of any registered source
triggers the forwarding (at most one $/cancel_request is ever sent
per request). Sources are observed while the response is being
awaited — that is, once the handle is consumed with
block_task,
on_receiving_result, or
forward_response_to; a source that was
already cancelled by then is honored immediately.
Source§impl<T> SentRequest<T>
impl<T> SentRequest<T>
Sourcepub fn map<U>(
self,
map_fn: impl FnOnce(T) -> Result<U, Error> + 'static + Send,
) -> SentRequest<U>where
T: 'static,
pub fn map<U>(
self,
map_fn: impl FnOnce(T) -> Result<U, Error> + 'static + Send,
) -> SentRequest<U>where
T: 'static,
Map a successful JSON-RPC response into an application type.
The mapped type does not need to implement JsonRpcResponse. The
mapper runs at most once and may consume captured state. JSON-RPC error
responses bypass the mapper. The mapped type may carry a non-'static
lifetime when it is consumed with block_task;
callback-style consumption still requires a 'static mapped type
because its work is spawned onto the connection.
Sourcepub fn forward_response_to(self, responder: Responder<T>) -> Result<(), Error>where
T: JsonRpcResponse,
pub fn forward_response_to(self, responder: Responder<T>) -> Result<(), Error>where
T: JsonRpcResponse,
Forward the response (success or error) to a request context when it arrives.
This is a convenience method for proxying messages between connections. When the response arrives, it will be automatically sent to the provided request context, whether it’s a successful response or an error.
§Example: Proxying requests
// Set up backend connection builder
let backend = UntypedRole.builder()
.on_receive_request(async |req: MyRequest, responder, cx| {
responder.respond(MyResponse { status: "ok".into() })
}, agent_client_protocol::on_receive_request!());
// Spawn backend and get a context to send to it
let backend_connection = cx.spawn_connection(backend, MockTransport)?;
// Set up proxy that forwards requests to backend
UntypedRole.builder()
.on_receive_request({
let backend_connection = backend_connection.clone();
async move |req: MyRequest, responder, cx| {
// Forward the request to backend and proxy the response back
backend_connection.send_request(req)
.forward_response_to(responder)?;
Ok(())
}
}, agent_client_protocol::on_receive_request!());§Type Safety
The request context’s response type must match the request’s response type, ensuring type-safe message forwarding.
§When to Use
Use this when:
- You’re implementing a proxy or gateway pattern
- You want to forward responses without processing them
- The response types match between the outgoing request and incoming request
This is equivalent to calling on_receiving_result and manually forwarding
the result, with two proxy-specific additions:
- If the pending response cannot be delivered, the incoming request is answered with an internal error instead of being left unanswered. Known clean incoming EOF is delivered like any other response error; an unexpected response-channel loss is forwarded as an outer consumption error.
- When the peer cancels the incoming request, the cancellation is
forwarded to the outgoing request, and the downstream response
(normal data or a cancellation error) is still forwarded back. This is
equivalent to registering the responder’s marker with
forward_cancellation_from.
Sourcepub async fn block_task(self) -> Result<T, Error>
pub async fn block_task(self) -> Result<T, Error>
Block the current task until the response is received.
Warning: This method blocks the current async task. It is safe only when that task
already runs outside the dispatch loop, such as the foreground future passed to
connect_with or a task created with ConnectionTo::spawn. Using it directly in a
handler callback will deadlock the connection.
§Safe Usage (outside the dispatch loop)
connection.on_receive_request(async |req: MyRequest, responder, cx| {
// Spawn a task to handle the request
cx.spawn({
let connection = cx.clone();
async move {
// Safe: We're in a spawned task, not blocking the event loop
let response = connection.send_request(OtherRequest {})
.block_task()
.await?;
// Process the response...
Ok(())
}
})?;
// Respond immediately
responder.respond(MyResponse { status: "ok".into() })
}, agent_client_protocol::on_receive_request!())§Unsafe Usage (in handlers - will deadlock!)
connection.on_receive_request(async |req: MyRequest, responder, cx| {
// ❌ DEADLOCK: Handler blocks event loop, which can't process the response
let response = cx.send_request(OtherRequest {})
.block_task()
.await?;
responder.respond(MyResponse { status: response.value })
}, agent_client_protocol::on_receive_request!())§When to Use
Use this method when:
- Your current task already runs outside the dispatch loop
- You need the response value to proceed with your logic
- Linear control flow is more natural than callbacks
For handler callbacks, use on_receiving_result instead.
Sourcepub fn on_receiving_ok_result<F>(
self,
responder: Responder<T>,
task: impl FnOnce(T, Responder<T>) -> F + 'static + Send,
) -> Result<(), Error>
pub fn on_receiving_ok_result<F>( self, responder: Responder<T>, task: impl FnOnce(T, Responder<T>) -> F + 'static + Send, ) -> Result<(), Error>
Schedule an async task to run when a successful response is received.
This is a convenience wrapper around on_receiving_result
for the common pattern of forwarding errors to a request context while only processing
successful responses.
§Behavior
- If the response is
Ok(value), your task receives the value and the request context - If the response is
Err(error), the error is automatically sent toresponderand your task is not called
§Example: Chaining requests
connection.on_receive_request(async |req: ValidateRequest, responder, cx| {
// Send initial request
cx.send_request(ValidateRequest { data: req.data.clone() })
.on_receiving_ok_result(responder, async |validation, responder| {
// Only runs if validation succeeded
if validation.is_valid {
// Respond to original request
responder.respond(ValidateResponse { is_valid: true, error: None })
} else {
responder.respond_with_error(agent_client_protocol::util::internal_error("validation failed"))
}
})?;
Ok(())
}, agent_client_protocol::on_receive_request!())§Ordering
Like on_receiving_result, response handling holds the
dispatch loop through callback completion when ordered consumption is selected before a
peer response is routed during its original dispatch. Pending-request failures delivered
without an incoming response and delayed routes do not carry that barrier. The callback
must not await later inbound traffic on the same connection. See the
ordering module for details.
§When to Use
Use this when:
- You need to respond to a request based on another request’s result
- You want errors to automatically propagate to the request context
- You only care about the success case
For more control over error handling, use on_receiving_result.
Sourcepub fn on_receiving_result<F>(
self,
task: impl FnOnce(Result<T, Error>) -> F + 'static + Send,
) -> Result<(), Error>
pub fn on_receiving_result<F>( self, task: impl FnOnce(Result<T, Error>) -> F + 'static + Send, ) -> Result<(), Error>
Register an async callback to run when the response is received.
This is the recommended way to select response handling from inside a handler because registration returns immediately. The response-consumption task waits concurrently for the response; once the response is dispatched, the ordered callback may hold the dispatch loop until it completes.
§Example: Handle response in callback
connection.on_receive_request(async |req: MyRequest, responder, cx| {
// Send a request and schedule a callback for the response
cx.send_request(QueryRequest { id: 22 })
.on_receiving_result({
let connection = cx.clone();
async move |result| {
match result {
Ok(response) => {
println!("Got response: {:?}", response);
// Can send more messages here
connection.send_notification(QueryComplete {})?;
Ok(())
}
Err(error) => {
eprintln!("Request failed: {}", error);
Err(error)
}
}
}
})?;
// Handler continues immediately after registering the callback
responder.respond(MyResponse { status: "processing".into() })
}, agent_client_protocol::on_receive_request!())§Ordering
When ordered consumption is selected before a peer response is routed during its original dispatch, the callback runs in a connection-managed task and the dispatch loop waits for it to complete before processing the next message.
The barrier does not apply when the pending request is failed without an incoming response,
such as on EOF. If the response was already routed, or an interceptor routes a retained
ResponseRouter after its original dispatch, the callback still runs but cannot
retroactively block messages that were already released.
While the barrier is held, the callback must not await a later response, notification, or
other inbound traffic on the same connection: that traffic cannot be dispatched until the
callback completes. Spawn follow-up work with ConnectionTo::spawn and return, or use
block_task from a task already outside the dispatch loop.
This differs from block_task, which does not select ordered
consumption: dispatch remains free while the caller processes the delivered response.
See the ordering module for details on ordering guarantees
and how to avoid deadlocks.
§Error Handling
If the scheduled task returns Err, the entire server will shut down. Make sure to handle
errors appropriately within your task.
§When to Use
Use this method when:
- You need to register response handling from a handler callback
- You want a peer response callback to complete before later messages are dispatched
- The callback performs bounded work that does not depend on later inbound traffic
When already outside the dispatch loop and you do not need ordering guarantees, consider
block_task.