grpc/grpc-java · error · S2AConnectionException

No valid response received from S2A.

Error message

No valid response received from S2A.

What it means

After a successful (zero-status) S2A response, sign still requires the SessionResp to contain an offload_private_key_operation_resp payload. If the field is absent, S2AConnectionException('No valid response received from S2A.') is thrown — S2A answered, but not with the expected signing result.

Source

Thrown at s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethod.java:138

            .setOffloadPrivateKeyOperationReq(
                OffloadPrivateKeyOperationReq.newBuilder()
                    .setOperation(OffloadPrivateKeyOperationReq.PrivateKeyOperation.SIGN)
                    .setSignatureAlgorithm(s2aSignatureAlgorithm)
                    .setRawBytes(ByteString.copyFrom(input)));
    if (localIdentity.isPresent()) {
      reqBuilder.setLocalIdentity(localIdentity.get().getIdentity());
    }

    SessionResp resp = stub.send(reqBuilder.build());

    if (resp.hasStatus() && resp.getStatus().getCode() != 0) {
      throw new S2AConnectionException(
          String.format(
              "Error occurred in response from S2A, error code: %d, error message: \"%s\".",
              resp.getStatus().getCode(), resp.getStatus().getDetails()));
    }
    if (!resp.hasOffloadPrivateKeyOperationResp()) {
      throw new S2AConnectionException("No valid response received from S2A.");
    }
    return resp.getOffloadPrivateKeyOperationResp().getOutBytes().toByteArray();
  }

  @Override
  public byte[] decrypt(SSLEngine engine, byte[] input) {
    throw new UnsupportedOperationException("decrypt is not supported.");
  }
}

View on GitHub (pinned to 64daddc1f3)

Solutions

  1. Upgrade both the S2A service and gRPC s2a client to matching handshaker proto versions so the offload response field is populated consistently.
  2. Check S2A server logs for the request; confirm the server actually performed the private-key operation.
  3. Retry the operation once — if it recurs deterministically, treat it as an S2A service defect and report with the captured SessionResp shape.
  4. Validate the response contract: assert hasOffloadPrivateKeyOperationResp() before consuming, so the failure is explicit and diagnosable.

Example fix

// before
byte[] out = resp.getOffloadPrivateKeyOperationResp().getOutBytes().toByteArray();
// after
if (!resp.hasOffloadPrivateKeyOperationResp()) {
  throw new IllegalStateException("S2A returned no offload response");
}
byte[] out = resp.getOffloadPrivateKeyOperationResp().getOutBytes().toByteArray();
Defensive patterns

Strategy: try-catch

Try / catch

try {
  byte[] sig = keyMethod.sign(engine, input);
} catch (S2AConnectionException e) {
  if ("No valid response received from S2A.".equals(e.getMessage())) {
    retryOnceThenFail(e); // likely proto/version skew or server bug
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Calling sign(engine, bytes) where resp has status code 0 (or no status) but !resp.hasOffloadPrivateKeyOperationResp() — e.g. S2A returned an empty/default response, the response type is wrong (e.g. a session-resume or error variant), or request/response field mismatch due to proto version skew.

Common situations: S2A service bug or misrouting returning an empty SessionResp; handshaker proto version skew so the client reads a field the server never set; S2A instance replying before performing the operation; response corruption where hasStatus() is false and payload missing.

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