grpc/grpc-java · error · IllegalStateException
Cannot alter onReadyHandler after call started. Use…
Error message
Cannot alter onReadyHandler after call started. Use ClientResponseObserver
What it means
ClientCalls' ClientCallStreamObserver implementation freezes its configuration once the call starts, and setOnReadyHandler throws IllegalStateException if called afterward. To receive on-ready callbacks set from within the response observer, gRPC requires passing a ClientResponseObserver whose beforeStart hook is invoked before the call starts.
Solutions
- Implement the response observer as ClientResponseObserver and set the on-ready handler in beforeStart() before the call starts
- Move all setOnReadyHandler/disableAutoInboundFlowControl calls into beforeStart
- If dynamic handlers are truly needed, wrap the Runnable and swap its target instead of replacing the handler
Example fix
// before
responseObserver.setOnReadyHandler(() -> sendMore()); // throws after start
// after
new ClientResponseObserver<Req, Resp>() {
public void beforeStart(ClientCallStreamObserver<Req> obs) { obs.setOnReadyHandler(() -> sendMore()); }
public void onNext(Resp v) {}
...
} Defensive patterns
Strategy: type-guard
Validate before calling
if (observer instanceof ClientCallStreamObserver) { boolean frozen = ((ClientCallStreamObserver<?>) observer).isReady(); /* configure via ClientResponseObserver.beforeStart instead */ } Type guard
boolean isFrozen(ClientCallStreamObserver<?> o) { return o instanceof ClientCallStreamObserver<?> && o.isReady(); } Try / catch
try { observer.setOnReadyHandler(h); } catch (IllegalStateException e) { throw new IllegalStateException("Set handler in ClientResponseObserver.beforeStart", e); } Prevention
- Always use ClientResponseObserver when needing stream configuration
- Set all onReady handlers in beforeStart
- Never configure observers from outside observer callbacks
When it happens
Trigger: Calling setOnReadyHandler on the ClientCallStreamObserver after the RPC has started (e.g. after asyncServerStreamingCall/asyncBidiStreamingCall returned and invoked onNext/onReady).
Common situations: Trying to install flow-control handlers lazily; migrating from RequestObserver patterns where the handler is set outside the observer; misunderstanding the ClientResponseObserver.beforeStart contract.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- 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…
- Invalid initial window size
AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08).
Data as JSON: /api/errors/f1b70a1253825ed9.
Report an issue: GitHub.
Appendix: source
Thrown at stub/src/main/java/io/grpc/stub/ClientCalls.java:491
call.cancel("Cancelled by client with StreamObserver.onError()", t);
aborted = true;
}
@Override
public void onCompleted() {
call.halfClose();
completed = true;
}
@Override
public boolean isReady() {
return call.isReady();
}
@Override
public void setOnReadyHandler(Runnable onReadyHandler) {
if (frozen) {
throw new IllegalStateException(
"Cannot alter onReadyHandler after call started. Use ClientResponseObserver");
}
this.onReadyHandler = onReadyHandler;
}
@Override
public void disableAutoInboundFlowControl() {
disableAutoRequestWithInitial(1);
}
@Override
public void disableAutoRequestWithInitial(int request) {
if (frozen) {
throw new IllegalStateException(
"Cannot disable auto flow control after call started. Use ClientResponseObserver");
}
Preconditions.checkArgument(request >= 0, "Initial requests must be non-negative");
initialRequest = request;View on GitHub (pinned to 64daddc1f3)