grpc/grpc-java · error · IOException

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

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.

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.

Appendix: 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)