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

  1. Stop writing once the call is completed, cancelled, or its deadline expired; check call.isCancelled() before sending
  2. Ensure each request stream is written by a single thread or properly sequenced; AsyncSink is not safe for concurrent writers
  3. Handle onError callbacks by aborting your send loop instead of continuing to write
  4. 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

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


AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08). Data as JSON: /api/errors/375de4a1af6336e3. Report an issue: GitHub.