grpc/grpc-java · error · S2AConnectionException

Error occurred in response from S2A, error code: %d, error m

Error message

Error occurred in response from S2A, error code: %d, error message: "%s".

What it means

S2APrivateKeyMethod.sign sends a SessionReq to the S2A offload service and checks the returned SessionResp status. If S2A reports a non-zero status code, it throws S2AConnectionException carrying the S2A error code and details — meaning the offload request failed on the S2A side rather than in the local TLS stack.

Source

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

      throws IOException, InterruptedException {
    checkArgument(input.length > 0, "No bytes to sign.");
    SignatureAlgorithm s2aSignatureAlgorithm =
        convertOpenSslSignAlgToS2ASignAlg(signatureAlgorithm);
    SessionReq.Builder reqBuilder =
        SessionReq.newBuilder()
            .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. Read the error code and details in the exception message and look them up in the S2A handshaker status documentation to identify the server-side failure.
  2. Verify the local identity configured for the channel matches a certificate/key actually provisioned in S2A (SPIFFE ID, cert chain).
  3. Check S2A service logs around the failure for the corresponding request; confirm the target S2A instance is healthy.
  4. Add retry/backoff for transient codes and fail fast (alert) on persistent identity/key errors.

Example fix

// before
byte[] sig = keyMethod.sign(engine, input); // throws on any S2A error
// after
try {
  byte[] sig = keyMethod.sign(engine, input);
} catch (S2AConnectionException e) {
  if (isTransient(e)) retry(); else failHandshake(e);
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
  byte[] sig = keyMethod.sign(engine, input);
} catch (S2AConnectionException e) {
  if (isTransientCode(e)) {
    retryWithBackoff();
  } else {
    alertAndFailHandshake(e); // persistent identity/key problem
  }
}

Prevention

When it happens

Trigger: Calling sign(engine, bytes) during an S2A-offloaded handshake when resp.hasStatus() is true and status code != 0 — e.g. S2A cannot access the private key, the identity (localIdentity) is wrong or not provisioned, the handshake context is stale, or the S2A service rejected the request.

Common situations: S2A deployment lacks the certificate/key referenced by the local identity; workload identity mismatch (wrong SPIFFE ID in s2a config); S2A and application version skew; transient S2A backend errors surfaced with a specific code.

Related errors


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