{"record":{"id":"7536eabec4390b5b","repo":"github/copilot-sdk","slug":"llm-inference-response-writeresponse-called-afte","errorCode":null,"errorMessage":"LLM inference response writeResponse() called after endResponse()/errorResponse()","messagePattern":"LLM inference response writeResponse\\(\\) called after endResponse\\(\\)/errorResponse\\(\\)","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"java/sdk/src/main/java/com/github/copilot/LlmInferenceExchange.java","lineNumber":238,"sourceCode":"                return;\n            }\n            finished = true;\n        }\n        var error = new LlmInferenceHttpResponseChunkError(message, code);\n        var params = new LlmInferenceHttpResponseChunkParams(requestId, \"\", null, Boolean.TRUE, error);\n        join(api().httpResponseChunk(params));\n    }\n\n    private void writeChunk(String data, boolean binary) throws IOException {\n        synchronized (lock) {\n            if (cancelled) {\n                throw new IOException(\"LLM inference request was cancelled by the runtime\");\n            }\n            if (!started) {\n                throw new IOException(\"LLM inference response writeResponse() called before startResponse()\");\n            }\n            if (finished) {\n                throw new IOException(\n                        \"LLM inference response writeResponse() called after endResponse()/errorResponse()\");\n            }\n        }\n        var params = new LlmInferenceHttpResponseChunkParams(requestId, data, binary ? Boolean.TRUE : null,\n                Boolean.FALSE, null);\n        join(api().httpResponseChunk(params));\n    }\n\n    private ServerLlmInferenceApi api() throws IOException {\n        ServerLlmInferenceApi api = rpcSupplier.get();\n        if (api == null) {\n            throw new IOException(\"LLM inference response used after RPC connection closed\");\n        }\n        return api;\n    }\n\n    private static <T> T join(CompletableFuture<T> future) throws IOException {\n        try {","sourceCodeStart":220,"sourceCodeEnd":256,"githubUrl":"https://github.com/github/copilot-sdk/blob/cd8cf15dc3f9e762615790aaed0a771a0f392755/java/sdk/src/main/java/com/github/copilot/LlmInferenceExchange.java#L220-L256","documentation":"This IOException is thrown when writeResponseText/writeResponseBinary is called after the exchange has already been finished via endResponse() or errorResponse(). Once finished, the response stream is complete and no further chunks can be sent over the RPC connection; the library rejects the write to keep the protocol consistent.","triggerScenarios":"Calling writeResponseText or writeResponseBinary after endResponse() or errorResponse() returned — e.g., writing a final summary chunk after closing the stream, or an async callback firing after the response was finalized.","commonSituations":"Async/continuation code that writes a trailing chunk after endResponse; duplicated finalize logic where endResponse is called then more writes happen in a finally block; retried handlers re-invoking write after the first attempt completed the response.","solutions":["Reorder code so all chunks are written before endResponse()/errorResponse()","Remove any writes in finally/cleanup blocks after the response is finished","Track completion state in your handler and skip writes once endResponse has been called","Catch this IOException and log rather than retrying — the exchange cannot be reopened"],"exampleFix":"// before\nexchange.writeResponseText(\"done\");\nexchange.endResponse();\n// after\nexchange.writeResponseText(\"done\");\nexchange.endResponse(); // no writes after this point","handlingStrategy":"validation","validationCode":"if (!responseFinished) {\n    exchange.writeResponseText(chunk);\n}","typeGuard":null,"tryCatchPattern":"try {\n    exchange.writeResponseText(chunk);\n} catch (IOException e) {\n    if (String.valueOf(e.getMessage()).contains(\"after endResponse\")) {\n        LOG.warning(\"skipped write after response finished\");\n        return;\n    }\n    throw e;\n}","preventionTips":["Mark the exchange finished in your own state and skip writes afterwards","No writes inside finally blocks once endResponse ran","Finish the response exactly once, at a single call site","Cancel async tasks that may write after finalize"],"tags":["io","lifecycle","api-misuse"],"backgroundTag":"invalid-state-transition","analyzedSha":"cd8cf15dc3f9e762615790aaed0a771a0f392755","analyzedAt":"2026-09-09T18:32:31.973Z","contentChangedAt":"2026-09-09T18:32:31.973Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}