grpc/grpc-java · error · ConnectionClosedException

Stream to the S2A is closed.

Error message

Stream to the S2A is closed.

What it means

S2AStub.send() throws ConnectionClosedException when the bidirectional stream to the S2A process has already been fully closed ( both writer and reader are done ). Attempting to send a SessionReq over a closed stream cannot succeed, so the call fails fast. The message is also logged at INFO before throwing.

Source

Thrown at s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java:110

  }

  /**
   * Sends a request and returns the response. Caller must wait until this method executes prior to
   * calling it again. If this method throws {@code ConnectionClosedException}, then it should not
   * be called again, and both {@code reader} and {@code writer} are closed.
   *
   * @param req the {@code SessionReq} message to be sent to the S2A server.
   * @return the {@code SessionResp} message received from the S2A server.
   * @throws ConnectionClosedException if {@code reader} or {@code writer} calls their {@code
   *     onCompleted} method.
   * @throws IOException if an unexpected response is received, or if the {@code reader} or {@code
   *     writer} calls their {@code onError} method.
   */
  @SuppressWarnings("CheckReturnValue")
  public SessionResp send(SessionReq req) throws IOException, InterruptedException {
    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.");
      }
    }

View on GitHub (pinned to 64daddc1f3)

Solutions

  1. Recreate the S2AStub ( and its underlying channel/stream ) after it is closed, then retry the request.
  2. Check stub liveness ( or catch ConnectionClosedException ) before reusing a cached stub.
  3. Ensure the S2A process is running and the channel to it has not been shut down.

Example fix

// before
resp = cachedStub.send(req);
// after
try {
  resp = cachedStub.send(req);
} catch (ConnectionClosedException e) {
  cachedStub = S2AStubFactory.create(channel);
  resp = cachedStub.send(req);
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (stub == null || stub.isShutdown()) {
  stub = stubFactory.create(channel);
}

Try / catch

try {
  resp = stub.send(req);
} catch (ConnectionClosedException e) {
  stub = stubFactory.create(channel); // rebuild and retry once
  resp = stub.send(req);
}

Prevention

When it happens

Trigger: Calling S2AStub.send(SessionReq) after the stub's stream to the S2A has finished writing and reading ( doneWriting && doneReading ), e.g. after shutdown() or after a previous stream error terminated both directions.

Common situations: Reusing a cached S2AStub whose gRPC stream already terminated; S2A process crash followed by continued validation calls; calling send() from checkPeerTrusted after a prior handshake closed the stream.

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