grpc/grpc-java · error · IOException

Received an unexpected response from a host at the S2A's add

Error message

Received an unexpected response from a host at the S2A's address. The S2A might be unavailable.

What it means

S2AStub.send() reads responses from the S2A stream via getResultOrThrow(). If that call raises an IOException, send() wraps it in a new IOException stating an unexpected response was received from a host at the S2A's address, suggesting the S2A might be unavailable. The original IOException is preserved as the cause.

Source

Thrown at s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java:122

   *     writer} calls their {@code onError} method.
   */
  @SuppressWarnings("CheckReturnValue")
  public SessionResp send(SessionReq req) throws IOException, InterruptedException {
    if (doneWriting && doneReading) {
      logger.log(Level.INFO, "Stream to the S2A is closed.");
      throw new ConnectionClosedException("Stream to the S2A is closed.");
    }
    createWriterIfNull();
    if (!responses.isEmpty()) {
      IOException exception = null;
      try {
        responses.take().getResultOrThrow();
      } catch (IOException e) {
        exception = e;
      }
      responses.clear();
      if (exception != null) {
        throw new IOException(
            "Received an unexpected response from a host at the S2A's address. The S2A might be"
                + " unavailable.", exception);
      } else {
        throw new IOException("Received an unexpected response from a host at the S2A's address.");
      }
    }
    try {
      writer.onNext(req);
    } catch (RuntimeException e) {
      writer.onError(e);
      responses.add(Result.createWithThrowable(e));
    }
    try {
      return responses.take().getResultOrThrow();
    } catch (ConnectionClosedException e) {
      // A ConnectionClosedException is thrown by getResultOrThrow when reader calls its
      // onCompleted method. The close method is called to also close the writer, and then the
      // ConnectionClosedException is re-thrown in order to indicate to the caller that send

View on GitHub (pinned to 64daddc1f3)

Solutions

  1. Verify the S2A address/port configuration points at the real S2A process.
  2. Check that the S2A process is running and healthy; restart it if it crashed.
  3. Inspect the cause chain for the underlying IOException and recreate the stub after a stream failure.

Example fix

// before
// S2A address from stale config
S2AStub stub = new S2AStub(channelTo("localhost:9000"));
// after
S2AStub stub = new S2AStub(channelTo(config.getS2AAddress())); // correct host/port
Defensive patterns

Strategy: retry

Validate before calling

// probe the S2A endpoint before handshakes
if (!s2aHealthCheck(address)) {
  throw new IllegalStateException("S2A not reachable/healthy at " + address);
}

Try / catch

try {
  resp = stub.send(req);
} catch (IOException e) {
  if (e.getCause() != null) {
    logger.warning("S2A stream failure: " + e.getCause());
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling S2AStub.send() and the pending SessionResp future completes exceptionally with an IOException — typically when the endpoint at the S2A's address is not actually the S2A ( wrong host/port ) or the S2A process died mid-stream.

Common situations: Misconfigured S2A address pointing at the wrong service; S2A process crashed between request and response; network interruption during a handshake validation.

Related errors


AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08). Data as JSON: /api/errors/46b770eb69b76531. Report an issue: GitHub.