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

  1. Call halfClose() exactly once, on a single code path
  2. Use a boolean flag or idempotent wrapper to prevent duplicate halfClose invocations
  3. 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

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


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