grpc/grpc-java · critical · CertificateException

Failed to send request to S2A.

Error message

Failed to send request to S2A.

What it means

S2ATrustManager.checkPeerTrusted() wraps any IOException from stub.send() into a CertificateException with the message 'Failed to send request to S2A.'. This converts stream-transport failures to the S2A into the X509 TrustManager API's checked exception type so the TLS stack can fail the handshake.

Source

Thrown at s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2ATrustManager.java:154

              .addAllCertificateChain(certificateChainToDerChain(chain)));
    } else {
      validatePeerCertificateChainReq.setServerPeer(
          ValidatePeerCertificateChainReq.ServerPeer.newBuilder()
              .addAllCertificateChain(certificateChainToDerChain(chain))
              .setServerHostname(hostname));
    }

    SessionReq.Builder reqBuilder =
        SessionReq.newBuilder().setValidatePeerCertificateChainReq(validatePeerCertificateChainReq);
    if (localIdentity.isPresent()) {
      reqBuilder.setLocalIdentity(localIdentity.get().getIdentity());
    }

    SessionResp resp;
    try {
      resp = stub.send(reqBuilder.build());
    } catch (IOException e) {
      throw new CertificateException("Failed to send request to S2A.", e);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      throw new CertificateException("Failed to send request to S2A.", e);
    }
    if (resp.hasStatus() && resp.getStatus().getCode() != 0) {
      throw new CertificateException(
          String.format(
              "Error occurred in response from S2A, error code: %d, error message: %s.",
              resp.getStatus().getCode(), resp.getStatus().getDetails()));
    }

    if (!resp.hasValidatePeerCertificateChainResp()) {
      throw new CertificateException("No valid response received from S2A.");
    }

    ValidatePeerCertificateChainResp validationResult = resp.getValidatePeerCertificateChainResp();
    if (validationResult.getValidationResult()
        != ValidatePeerCertificateChainResp.ValidationResult.SUCCESS) {

View on GitHub (pinned to 64daddc1f3)

Solutions

  1. Ensure the S2A process is running and reachable at the configured address.
  2. Recreate the S2AStub/channel after a ConnectionClosedException before continuing handshakes.
  3. Catch CertificateException in your trust-manager wrapper and fall back to a local trust store if S2A offload is optional.

Example fix

// before
resp = stub.send(req); // IOException propagates as CertificateException
// after
if (!stub.isOpen()) {
  stub = stubFactory.create(channel); // rebuild before validating
}
resp = stub.send(req);
Defensive patterns

Strategy: try-catch

Validate before calling

if (!isS2aProcessRunning()) {
  throw new IllegalStateException("Cannot run TLS handshake: S2A process is not running");
}

Try / catch

try {
  sslEngine.beginHandshake();
} catch (SSLHandshakeException e) {
  if (e.getCause() instanceof CertificateException
      && e.getCause().getMessage().contains("Failed to send request to S2A")) {
    // transport issue to S2A, not an actual cert failure — recreate stub and retry
  }
}

Prevention

When it happens

Trigger: checkClientTrusted() or checkServerTrusted() invoked during a TLS handshake while S2AStub.send() throws IOException — closed stream, unreachable S2A, or unexpected response ( see errors 231-233 ).

Common situations: TLS peer validation with S2A offload while the S2A process is down; stale cached stub after stream close; misaddressed S2A endpoint.

Understand the failure class

Background: ECONNREFUSED and "connection refused" / "could not connect to server" errors: what they mean and how to fix them — this error's family across 44 libraries.

Related errors


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