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.

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.

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