grpc/grpc-java · warning · IOException
closed
Error message
closed
What it means
okhttp's AsyncSink is a buffered sink that hands writes to a serializing writer thread. Once close() has been called, any further write(Buffer, long) throws IOException("closed"). It indicates the stream was already terminated (normally, cancelled, or after an error) and the caller attempted to send more data.
Source
Thrown at okhttp/src/main/java/io/grpc/okhttp/AsyncSink.java:102
* this method is scheduled in the executor. The socket is needed for closing.
*
* <p>should only be called once by thread of executor.
*/
void becomeConnected(Sink sink, Socket socket) {
checkState(this.sink == null, "AsyncSink's becomeConnected should only be called once.");
this.sink = checkNotNull(sink, "sink");
this.socket = checkNotNull(socket, "socket");
}
FrameWriter limitControlFramesWriter(FrameWriter delegate) {
return new LimitControlFramesWriter(delegate);
}
@Override
public void write(Buffer source, long byteCount) throws IOException {
checkNotNull(source, "source");
if (closed) {
throw new IOException("closed");
}
try (TaskCloseable ignore = PerfMark.traceTask("AsyncSink.write")) {
boolean closeSocket = false;
synchronized (lock) {
buffer.write(source, byteCount);
queuedControlFrames += controlFramesInWrite;
controlFramesInWrite = 0;
if (!controlFramesExceeded && queuedControlFrames > maxQueuedControlFrames) {
controlFramesExceeded = true;
closeSocket = true;
} else {
if (writeEnqueued || flushEnqueued || buffer.completeSegmentByteCount() <= 0) {
return;
}
writeEnqueued = true;
}
}View on GitHub (pinned to 64daddc1f3)
Solutions
- Stop writing once the call is completed, cancelled, or its deadline expired; check call.isCancelled() before sending
- Ensure each request stream is written by a single thread or properly sequenced; AsyncSink is not safe for concurrent writers
- Handle onError callbacks by aborting your send loop instead of continuing to write
- Wrap writes in try-catch on IOException and treat 'closed' as stream-already-done, not a retryable error
Example fix
// before
requestStream.send(nextMessage); // may throw IOException("closed") after cancel
// after
if (!call.isCancelled() && !done) {
try {
requestStream.send(nextMessage);
} catch (IOException e) {
if ("closed".equals(e.getMessage())) return; // stream already terminated
throw e;
}
} Defensive patterns
Strategy: try-catch
Validate before calling
if (call.isCancelled() || call.isDeadlineExceeded() || streamClosed) {
return; // do not attempt write
} Type guard
boolean canWrite(CallStreamObserver<?> obs) { return !obs.isCancelled() && obs.isReady(); } Try / catch
try {
sink.write(buffer, byteCount);
} catch (IOException e) {
if ("closed".equals(e.getMessage())) {
// stream already terminated; stop sending, no retry
return;
}
throw e;
} Prevention
- Stop sending as soon as onError/onCompleted/cancel fires
- Check call.isCancelled() before each send on long-lived streams
- Write request streams from a single thread or serialize access
- Treat 'closed' IOException as terminal, not retryable
When it happens
Trigger: Calling write() on the request stream after close() completed, after cancel() (e.g. call was cancelled or deadline hit), or after a previous write failure closed the sink internally.
Common situations: Client code continuing to send messages on a gRPC okhttp channel after onError/onCompleted or after cancelling the call; racing a timeout/cancellation with outgoing writes; forgetting that half-close ends the stream.
Related errors
- Unable to load OkHttpChannelProvider
- ProxySelector ${proxySelectorClass} returned ${nullOrEmptyLi
- Unable to perform write due to unavailable sink.
- TLS Provider failure
- Unknown negotiation type: ${negotiationType}
AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08).
Data as JSON: /api/errors/375de4a1af6336e3.
Report an issue: GitHub.