grpc/grpc-java · error · IllegalStateException
halfClose cannot be called after already half closed or canc
Error message
halfClose cannot be called after already half closed or cancelled
What it means
State-validation error in BlockingClientCall.halfClose: halfClose may only be called once to signal the end of client-side writes. The writeClosed flag is already true, meaning the call was either half-closed previously or cancelled (cancel() sets writeClosed). It fires when a caller's control flow sends messages after finishing/cancelling the stream, typically in blockingV2ServerStreamingCall usage.
Source
Thrown at stub/src/main/java/io/grpc/stub/BlockingClientCall.java:261
* Cancel stream and stop any further writes. Note that some reads that are in flight may still
* happen after the cancel.
*
* @param message if not {@code null}, will appear as the description of the CANCELLED status
* @param cause if not {@code null}, will appear as the cause of the CANCELLED status
*/
public void cancel(String message, Throwable cause) {
writeClosed = true;
call.cancel(message, cause);
}
/**
* Indicate that no more writes will be done and the stream will be closed from the client side.
*
* @see ClientCall#halfClose()
*/
public void halfClose() {
if (writeClosed) {
throw new IllegalStateException(
"halfClose cannot be called after already half closed or cancelled");
}
writeClosed = true;
call.halfClose();
}
/**
* Status that server sent when closing channel from its side.
*
* @return null if stream not closed by server, otherwise Status sent by server
*/
@VisibleForTesting
Status getClosedStatus() {
executor.drain();
CloseState state = closeState.get();
return (state == null) ? null : state.status;
}View on GitHub (pinned to 64daddc1f3)
Solutions
- Call halfClose() exactly once, on a single code path
- Use a boolean flag or idempotent wrapper to prevent duplicate halfClose invocations
- Check writeClosed (or catch IllegalStateException) before calling halfClose in cleanup paths
Example fix
// before
try { doStreaming(call); } finally { call.halfClose(); } // plus halfClose inside doStreaming -> throws
// after
boolean closed = false;
if (!closed) { call.halfClose(); closed = true; } Defensive patterns
Strategy: try-catch
Try / catch
try { call.halfClose(); } catch (IllegalStateException e) { /* already half-closed; ignore */ } Prevention
- Half-close exactly once per call
- Avoid half-closing in both success and finally paths
- Wrap halfClose in an idempotent helper
When it happens
Trigger: Calling halfClose() twice; calling halfClose() after cancel(); a helper method that half-closes being invoked on both a success path and a finally block.
Common situations: Idempotency assumptions in wrapper classes; error-handling code that half-closes before rethrowing while a finally block half-closes again; duplicated completion logic.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Framer already closed
- Writes cannot be done after calling halfClose or cancel
- Metric with name ${name} already exists
- ScheduledExecutorService not set in Builder
- ChannelLogger is not set in Builder
AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08).
Data as JSON: /api/errors/163a9724462c4350.
Report an issue: GitHub.