Server-streaming requests block until first message is received
Bug Report
Version
tonic v0.13.1 tonic-build v0.13.1
Platform
Linux arbiter 6.14.10-zen1-1-zen #1 ZEN SMP PREEMPT_DYNAMIC Wed, 04 Jun 2025 18:52:21 +0000 x86_64 GNU/Linux
Description
Upon requesting a streaming response, the (let's call it) "initial" call blocks until the first message is received, rather than .next() doing the blocking.
My code goes something like this :
let mut response = client.streaming_request(request.clone()).await?.into_inner();
while let Some(obj) = response.next().await { ... }
I would expect .next() to do all the blocking here (bar establishing the connection), but the blockage is split across .streaming_request() and .next(), the former blocking until the first streamed response, and the latter handling all subsequent responses.
The first one waits until it has gotten the initial headers which may also coincide with the first message.
And there is nothing to do about that? I have this trait that works around it
/// A trait that resolves a [`Future`] whose output is a result with a [`Stream`] and only starts
/// streaming if the result is ok.
pub trait Deferred<I, S, E>: Future<Output = Result<S, E>>
where
S: Stream<Item = I>,
{
/// Turn `self` into a stream with the same type as `S` in case the output is [`Result::ok`].
fn deferred(self) -> impl Stream<Item = I>;
}
impl<I, S, E, F> Deferred<I, S, E> for F
where
F: Future<Output = Result<S, E>>,
S: Stream<Item = I>,
E: std::fmt::Debug,
{
fn deferred(self) -> impl Stream<Item = I> {
FutureExt::into_stream(self)
.filter_map(|result| {
std::future::ready(match result {
Ok(stream) => Some(stream),
Err(err) => {
tracing::error!(?err, "failed to create stream");
None
}
})
})
.flatten()
}
}
but it's awkward, easy to miss and in some situations there are lifetime issues.
We have discussed it and its in the plans to look at this behavior and at least align it with other gRPC implementations.
Hi, we just came across this issue in v0.14.2. For reference, the golang version of the client does return immediately after invoking the method and only blocks until stream.Recv() is called.