You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
binder: ServerStream.close() marks the stream closed before the suffix is sent, so a flow-control-deferred send failure leaks the call #13097
If the server's close (status + trailers, plus any pending unary response) is deferred by transport flow control and the deferred send later fails, the stream should still end in the same clean way:
the client receives an out-of-band close
the call is removed from ongoingCalls so transport can be marked as no longer in use , and
the application is not told onComplete() for an RPC whose close was never sent.
Once closed() has been delivered to a ServerStreamListener, no further callbacks (onReady, in particular) should follow.
What did you see instead?
Both SingleMessageServerStream.close() and MultiMessageServerStream.close() do:
BinderTransport.isReady() is !flowController.isTransmitWindowFull(), a single 128 KiB window (TRANSACTION_BYTES_WINDOW) shared by every call on the transport. If another stream has filled it when close() runs, Outbound.canSend() is false, send() returns without writing, and the suffix stays queued until an ACKNOWLEDGE_BYTES arrives. inbound.onCloseSent() nevertheless runs immediately: it moves ServerInbound to State.CLOSED and calls listener.closed(Status.OK). Unregistration is deferred to ServerOutbound.writeSuffix(), which has not run.
When the window reopens, BinderTransport.handleAcknowledgedBytes() → Inbound.onTransportReady() first calls listener.onReady() (on a listener that has already been closed()), then outbound.onTransportReady() → sendInternal(). If that deferred send fails with a StatusExceptionbefore reaching writeSuffix()'s unregister() — e.g. MetadataHelper.writeMetadata() throwing RESOURCE_EXHAUSTED: Metadata value too large for a stream-marshalled trailer value of BlockPool.BLOCK_SIZE (16 KiB) or more, an IOException from the response stream in writeMessageData(), or a RemoteException on a non-final chunk of a multi-block response — then Inbound.onTransportReady() catches it and calls closeAbnormal(se.getStatus()), which is guarded by if (!isClosed()) and is now a no-op.
Additionally, any subsequent OOB cancel from the client for that call is dropped by Inbound.handleTransaction()'s if (isClosed()) return;, so client-side cleanup cannot repair the server. If the deferred send succeeds, writeSuffix() calls unregister() before sendTransaction(), so the normal path and a RemoteException on the suffix transaction itself do not leak; only failures before that point do. If the deferred send throws an unchecked exception, it escapes into BinderTransport.handleTransaction()'s catch (RuntimeException) from #10092 and terminates the whole transport — not a leak, but a per-stream problem taking down every call on the connection.
Note that the premature closed(OK) and the post-close onReady() happen for every flow-control-deferred close, not only the ones whose deferred send later fails.
Steps to reproduce the bug
Difficult to reproduce on a real device. It can be done in robolectric using a BlackHoleOneWayBinderProxy to force flow-control to kick in.
I have submitted PR #13117 with a fix for this issue. The changes ensure that the server stream is closed only after the suffix has been sent successfully, with proper cleanup if a deferred send fails. I have also added regression tests for the different failure and cancellation scenarios. All 493 Binder tests passed locally, with no failures. I would appreciate any feedback on the approach. Thanks!
Activity
kkmurthyt21 commentedon Oct 14, 2026
I have submitted PR #13117 with a fix for this issue. The changes ensure that the server stream is closed only after the suffix has been sent successfully, with proper cleanup if a deferred send fails. I have also added regression tests for the different failure and cancellation scenarios. All 493 Binder tests passed locally, with no failures. I would appreciate any feedback on the approach. Thanks!