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
- Ensure each send() result is consumed before the next request is issued (one request in flight at a time)
- Do not share the AltsHandshakerStub across threads — serialize handshake steps
- Close and rebuild the handshaker stub/connection; the queue state is unrecoverable once desynchronized
- 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
- Keep at most one in-flight handshaker request; consume each response before the next send
- Never share the stub across threads
- Rebuild the stub after any IOException
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
- Invalid frame length ${dataLength}
- Could not get enough key data from the handshake.
- Handshaker service error: ${status.getDetails()}
- No handshaker response received
- TsiHandshakeHandler encountered exception
AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08).
Data as JSON: /api/errors/bde48bb09c29ec68.
Report an issue: GitHub.