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
- Call sendMessage(message) (or ClientCall.sendMessage) with the typed message instead of writeMessage(InputStream)
- Route InputStream-based writes through grpc's serialization (MethodDescriptor.marshaller) via sendMessage
- 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
- Always send messages via ClientCall.sendMessage, never the internal ClientStream
- In interceptors, forward calls instead of bypassing the call abstraction
- Don't cast streams to ClientStream to write raw InputStreams
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
- Cannot alter onReadyHandler after call started. Use…
- Cannot disable auto flow control after call started. Use…
- ChannelLogger is not set in Builder
- Framer already closed
- halfClose cannot be called after already half closed or…
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)