{"record":{"id":"1fb63d15bbeca76b","repo":"grpc/grpc-java","slug":"writes-cannot-be-done-after-calling-halfclose-or-c","errorCode":null,"errorMessage":"Writes cannot be done after calling halfClose or cancel","messagePattern":"Writes cannot be done after calling halfClose or cancel","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"stub/src/main/java/io/grpc/stub/BlockingClientCall.java","lineNumber":221,"sourceCode":"   *\n   * @param request Message to send to the server\n   * @param timeout How long to wait before giving up.  Values &lt;= 0 are no wait\n   * @param unit A TimeUnit determining how to interpret the timeout parameter\n   * @return true if the request is sent to stream, false if skipped\n   * @throws TimeoutException if write does not become ready before the specified timeout expires\n   * @throws StatusException If the stream has closed in an error state\n   */\n  public boolean write(ReqT request, long timeout, TimeUnit unit)\n      throws InterruptedException, TimeoutException, StatusException {\n    long endNanoTime = System.nanoTime() + unit.toNanos(timeout);\n    return write(false, request, endNanoTime);\n  }\n\n  private boolean write(boolean waitForever, ReqT request, long endNanoTime)\n      throws InterruptedException, TimeoutException, StatusException {\n\n    if (writeClosed) {\n      throw new IllegalStateException(\"Writes cannot be done after calling halfClose or cancel\");\n    }\n\n    Predicate<BlockingClientCall<ReqT, RespT>> predicate =\n        (x) -> x.call.isReady() || x.closeState.get() != null;\n    executor.waitAndDrainWithTimeout(waitForever, endNanoTime, predicate, this);\n    CloseState savedCloseState = closeState.get();\n    if (savedCloseState == null) {\n      call.sendMessage(request);\n      return true;\n    } else if (savedCloseState.status.isOk()) {\n      return false;\n    } else {\n      throw savedCloseState.status.asException(savedCloseState.trailers);\n    }\n  }\n\n  void sendSingleRequest(ReqT request) {\n    call.sendMessage(request);","sourceCodeStart":203,"sourceCodeEnd":239,"githubUrl":"https://github.com/grpc/grpc-java/blob/64daddc1f3d1975670f769f3e97bde8b2ba32d25/stub/src/main/java/io/grpc/stub/BlockingClientCall.java#L203-L239","documentation":"BlockingClientCall.write throws IllegalStateException if writeClosed is true. writeClosed is set when halfClose() or cancel() was already called, after which the client half of the stream is closed and further writes are invalid per gRPC stream semantics.","triggerScenarios":"Calling write() after halfClose(); calling write() after cancel(); reusing a finished request stream in a loop that keeps sending.","commonSituations":"Producer code that sends messages after signaling end-of-stream; cancel-on-timeout paths followed by a final write; retry logic resending after the stream was closed.","solutions":["Track stream completion and stop calling write() once halfClose() has been issued","Move all writes before the single final halfClose() call","Create a new BlockingClientCall (new RPC) if more writes are needed after cancellation","Guard writes with a try/catch on IllegalStateException in cleanup code"],"exampleFix":"// before\ncall.halfClose();\ncall.write(req); // throws\n// after\ncall.write(req);\ncall.halfClose(); // last write first, then half-close","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { call.write(req); } catch (IllegalStateException e) { // stream already closed; drop or open new RPC }","preventionTips":["Issue halfClose only after all writes","Track cancelled/half-closed state in producer loops","Open a new RPC after cancellation instead of reusing"],"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"}