{"record":{"id":"163a9724462c4350","repo":"grpc/grpc-java","slug":"halfclose-cannot-be-called-after-already-half-clos","errorCode":null,"errorMessage":"halfClose cannot be called after already half closed or cancelled","messagePattern":"halfClose cannot be called after already half closed or cancelled","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"stub/src/main/java/io/grpc/stub/BlockingClientCall.java","lineNumber":261,"sourceCode":"   * Cancel stream and stop any further writes.  Note that some reads that are in flight may still\n   * happen after the cancel.\n   *\n   * @param message if not {@code null}, will appear as the description of the CANCELLED status\n   * @param cause if not {@code null}, will appear as the cause of the CANCELLED status\n   */\n  public void cancel(String message, Throwable cause) {\n    writeClosed = true;\n    call.cancel(message, cause);\n  }\n\n  /**\n   * Indicate that no more writes will be done and the stream will be closed from the client side.\n   *\n   * @see ClientCall#halfClose()\n   */\n  public void halfClose() {\n    if (writeClosed) {\n      throw new IllegalStateException(\n          \"halfClose cannot be called after already half closed or cancelled\");\n    }\n\n    writeClosed = true;\n    call.halfClose();\n  }\n\n  /**\n   * Status that server sent when closing channel from its side.\n   *\n   * @return null if stream not closed by server, otherwise Status sent by server\n   */\n  @VisibleForTesting\n  Status getClosedStatus() {\n    executor.drain();\n    CloseState state = closeState.get();\n    return (state == null) ? null : state.status;\n  }","sourceCodeStart":243,"sourceCodeEnd":279,"githubUrl":"https://github.com/grpc/grpc-java/blob/64daddc1f3d1975670f769f3e97bde8b2ba32d25/stub/src/main/java/io/grpc/stub/BlockingClientCall.java#L243-L279","documentation":"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.","triggerScenarios":"Calling halfClose() twice; calling halfClose() after cancel(); a helper method that half-closes being invoked on both a success path and a finally block.","commonSituations":"Idempotency assumptions in wrapper classes; error-handling code that half-closes before rethrowing while a finally block half-closes again; duplicated completion logic.","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"],"exampleFix":"// before\ntry { doStreaming(call); } finally { call.halfClose(); } // plus halfClose inside doStreaming -> throws\n// after\nboolean closed = false;\nif (!closed) { call.halfClose(); closed = true; }","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { call.halfClose(); } catch (IllegalStateException e) { /* already half-closed; ignore */ }","preventionTips":["Half-close exactly once per call","Avoid half-closing in both success and finally paths","Wrap halfClose in an idempotent helper"],"tags":["grpc","streaming","illegal-state"],"backgroundTag":"invalid-state-transition","analyzedSha":"64daddc1f3d1975670f769f3e97bde8b2ba32d25","analyzedAt":"2026-09-08T06:14:57.704Z","contentChangedAt":"2026-09-08T06:14:57.704Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}