grpc/grpc-java · error · IllegalStateException

RetriableStream.writeMessage() should not be called directly

Error message

RetriableStream.writeMessage() should not be called directly

What it means

RetriableStream buffers messages internally (sendMessage) rather than taking raw InputStreams, so the ClientStream.writeMessage(InputStream) entry point is deliberately blocked. It's marked final and always throws IllegalStateException to catch misuse.

Solutions

  1. Call sendMessage(message) (or ClientCall.sendMessage) with the typed message instead of writeMessage(InputStream)
  2. Route InputStream-based writes through grpc's serialization (MethodDescriptor.marshaller) via sendMessage
  3. If writing an interceptor, skip RetriableStream special handling and only wrap the underlying stream's writeMessage

Example fix

// before
((ClientStream) call).writeMessage(marshaller.stream(message));
// after
call.sendMessage(message); // ClientCall#sendMessage, which buffers for retry
Defensive patterns

Strategy: type-guard

Validate before calling

if (stream instanceof RetriableStream) { /* use sendMessage(Object), not writeMessage(InputStream) */ }

Type guard

static boolean isRetriable(ClientStream s) { return s instanceof RetriableStream; }

Try / catch

try { stream.writeMessage(in); } catch (IllegalStateException e) { if (e.getMessage().contains("should not be called directly")) { /* switch to sendMessage */ } else throw e; }

Prevention

When it happens

Trigger: Calling retriableStream.writeMessage(InputStream) directly instead of sendMessage(Object) — typically when custom stream interception code treats the call as a plain ClientStream.

Common situations: Writing a custom ClientInterceptor or transport wrapper that forwards writeMessage() on the retriable stream; reflection-based code invoking the ClientStream interface method.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

Thrown at core/src/main/java/io/grpc/internal/RetriableStream.java:574

    synchronized (lock) {
      if (!state.passThrough) {
        state.buffer.add(bufferEntry);
      }
      savedDrainedSubstreams = state.drainedSubstreams;
    }

    for (Substream substream : savedDrainedSubstreams) {
      bufferEntry.runWith(substream);
    }
  }

  /**
   * Do not use it directly. Use {@link #sendMessage(Object)} instead because we don't use
   * InputStream for buffering.
   */
  @Override
  public final void writeMessage(InputStream message) {
    throw new IllegalStateException("RetriableStream.writeMessage() should not be called directly");
  }

  final void sendMessage(final ReqT message) {
    State savedState = state;
    if (savedState.passThrough) {
      savedState.winningSubstream.stream.writeMessage(method.streamRequest(message));
      return;
    }

    class SendMessageEntry implements BufferEntry {
      @Override
      public void runWith(Substream substream) {
        substream.stream.writeMessage(method.streamRequest(message));
        // TODO(ejona): Workaround Netty memory leak. Message writes always need to be followed by
        // flushes (or half close), but retry appears to have a code path that the flushes may
        // not happen. The code needs to be fixed and this removed. See #9340.
        substream.stream.flush();
      }

View on GitHub (pinned to 64daddc1f3)