grpc/grpc-java · error · IOException

Received an unexpected response.

Error message

Received an unexpected response.

What it means

AltsHandshakerStub.send() is strictly request/response: it rejects a send when the responseQueue already holds an unconsumed response. Finding a queued response means the previous response was never taken (protocol desynchronization between client requests and handshaker responses), so it throws IOException("Received an unexpected response.").

Source

Thrown at alts/src/main/java/io/grpc/alts/internal/AltsHandshakerStub.java:65

  }

  @VisibleForTesting
  AltsHandshakerStub(StreamObserver<HandshakerReq> writer) {
    this.writer = writer;
    serviceStub = null;
  }

  @VisibleForTesting
  StreamObserver<HandshakerResp> getReaderForTest() {
    return reader;
  }

  /** Send a handshaker request and return the handshaker response. */
  public HandshakerResp send(HandshakerReq req) throws InterruptedException, IOException {
    createWriterIfNull();
    maybeThrowIoException();
    if (!responseQueue.isEmpty()) {
      throw new IOException("Received an unexpected response.");
    }

    writer.onNext(req);
    Optional<HandshakerResp> result = responseQueue.take();
    if (result.isPresent()) {
      return result.get();
    }

    if (exceptionMessage.get() != null) {
      throw new IOException(exceptionMessage.get().info, exceptionMessage.get().throwable);
    } else {
      throw new IOException("No handshaker response received");
    }
  }

  /** Create a new writer if the writer is null. */
  private void createWriterIfNull() {
    if (writer == null) {

View on GitHub (pinned to 64daddc1f3)

Solutions

  1. Ensure each send() result is consumed before the next request is issued (one request in flight at a time)
  2. Do not share the AltsHandshakerStub across threads — serialize handshake steps
  3. Close and rebuild the handshaker stub/connection; the queue state is unrecoverable once desynchronized
  4. Update grpc-alts: older versions had handshake-stream race fixes

Example fix

// before
stub.send(req1);
stub.send(req2); // response from req1 still queued
// after
HandshakerResp resp = stub.send(req1);
process(resp);
HandshakerResp resp2 = stub.send(req2); // send only after consuming previous response
Defensive patterns

Strategy: try-catch

Validate before calling

null

Type guard

null

Try / catch

try {
  HandshakerResp resp = stub.send(req);
} catch (IOException e) {
  stub.close();
  stub = newStub(handshakerAddr); // queue state is unrecoverable
}

Prevention

When it happens

Trigger: Calling send() while a previous HandshakerResp is still queued — e.g. sending a new HandshakerReq before consuming the prior response, or the handshaker service pushing an extra unsolicited response.

Common situations: Handshake driver bugs that call next()/send() out of order, duplicate responses from a misbehaving handshaker service, or concurrent use of the stub from multiple threads without synchronization.

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/bde48bb09c29ec68. Report an issue: GitHub.