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

  1. Implement the response observer as ClientResponseObserver and set the on-ready handler in beforeStart() before the call starts
  2. Move all setOnReadyHandler/disableAutoInboundFlowControl calls into beforeStart
  3. 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

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


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)