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
- Recreate the S2AStub ( and its underlying channel/stream ) after it is closed, then retry the request.
- Check stub liveness ( or catch ConnectionClosedException ) before reusing a cached stub.
- 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
- Do not cache S2AStub instances beyond the lifetime of their stream.
- Watch for S2A process health and recreate stubs on termination.
- Wrap all send() calls with a rebuild-and-retry helper.
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
- Received an unexpected response from a host at the S2A's add
- Received an unexpected response from a host at the S2A's add
- Failed to send request to S2A.
- Failed to get client TLS configuration from S2A.
- response from S2A server has ean error %d with error message
AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08).
Data as JSON: /api/errors/4ced16adc3a09a2f.
Report an issue: GitHub.