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.

What it means

When S2AStub.send() receives a failed response from the S2A stream without an IOException cause, it throws a plain IOException saying it received an unexpected response from a host at the S2A's address. This variant ( no 'S2A might be unavailable' suffix ) indicates the response failed but the failure type is not I/O related.

Source

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

    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
      // should not be called again.
      close();
      throw e;
    }

View on GitHub (pinned to 64daddc1f3)

Solutions

  1. Confirm the configured S2A address serves the actual S2A handshaker service.
  2. Check S2A process logs for the error it returned on the stream.
  3. Recreate the stub/channel and retry after the stream is reset.

Example fix

// before
// address points to unrelated service
channel = ManagedChannelBuilder.forTarget("10.0.0.5:9000").usePlaintext().build();
// after
channel = ManagedChannelBuilder.forTarget(s2aProcess.socketPath()).build();
Defensive patterns

Strategy: validation

Validate before calling

// ensure the gRPC channel speaks to the real S2A handshaker service
if (!channelHealthCheckForS2aService(channel)) {
  throw new IllegalStateException("Endpoint does not serve the S2A handshaker service");
}

Try / catch

try {
  resp = stub.send(req);
} catch (IOException e) {
  if (e.getCause() == null) {
    // unexpected non-IO failure: suspect wrong endpoint or error status
    logger.warning("Non-IO failure from S2A address: " + e.getMessage());
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling S2AStub.send() when responses.take().getResultOrThrow() throws a non-IOException failure, or the taken response is an error result without an IOException — e.g. a status error from the wrong/unexpected gRPC server at the S2A address.

Common situations: Something other than the S2A ( e.g. an unrelated gRPC server ) is listening at the configured address; S2A returned an error status on the stream.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


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