{"record":{"id":"d21659ce7ff51226","repo":"github/copilot-sdk","slug":"llm-inference-response-already-finished","errorCode":null,"errorMessage":"LLM inference response already finished","messagePattern":"LLM inference response already finished","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"java/sdk/src/main/java/com/github/copilot/LlmInferenceExchange.java","lineNumber":185,"sourceCode":"\n    byte[] drainBody() throws InterruptedException {\n        ByteArrayOutputStream out = new ByteArrayOutputStream();\n        BodyFrame frame;\n        while ((frame = readFrame()) != null) {\n            out.writeBytes(frame.data());\n        }\n        return out.toByteArray();\n    }\n\n    // --- Response emit (driven by the handler) ---\n\n    void startResponse(int status, String statusText, Map<String, List<String>> headers) throws IOException {\n        synchronized (lock) {\n            if (started) {\n                throw new IOException(\"LLM inference response startResponse() called twice\");\n            }\n            if (finished) {\n                throw new IOException(\"LLM inference response already finished\");\n            }\n            started = true;\n        }\n        var params = new LlmInferenceHttpResponseStartParams(requestId, (long) status, statusText, headers);\n        join(api().httpResponseStart(params));\n    }\n\n    void writeResponseText(String text) throws IOException {\n        writeChunk(text, false);\n    }\n\n    void writeResponseBinary(byte[] data) throws IOException {\n        writeChunk(Base64.getEncoder().encodeToString(data), true);\n    }\n\n    void writeResponseBinary(byte[] data, int offset, int length) throws IOException {\n        ByteBuffer encoded = Base64.getEncoder().encode(ByteBuffer.wrap(data, offset, length));\n        writeChunk(new String(encoded.array(), 0, encoded.limit(), StandardCharsets.ISO_8859_1), true);","sourceCodeStart":167,"sourceCodeEnd":203,"githubUrl":"https://github.com/github/copilot-sdk/blob/cd8cf15dc3f9e762615790aaed0a771a0f392755/java/sdk/src/main/java/com/github/copilot/LlmInferenceExchange.java#L167-L203","documentation":"LlmInferenceExchange.startResponse() also refuses to start a response after the exchange is already finished; it throws IOException('LLM inference response already finished') when the 'finished' flag is set under the lock. Once a response has fully completed, no further response lifecycle calls are accepted for that exchange.","triggerScenarios":"Calling startResponse() (via streamResponse or finalizeError) on an exchange whose response already finished — e.g. a delayed error handler firing after the body was completed (LlmInferenceExchange.java:185).","commonSituations":"Asynchronous timeout or error callbacks arriving after the exchange completed normally; retry logic mistakenly reusing a finished exchange object for a second attempt.","solutions":["Check the exchange's finished state before invoking any response method; abandon late error handling if finished.","Do not reuse LlmInferenceExchange objects across attempts — create a fresh exchange per request.","Catch this IOException in deferred callbacks and treat it as a no-op (the client already got a complete response)."],"exampleFix":"// before\nscheduledErrorRetry(exchange); // runs after exchange finished\n// after\nif (!exchange.isFinished()) {\n    scheduledErrorRetry(exchange);\n}","handlingStrategy":"type-guard","validationCode":"if (exchange.isFinished()) {\n    LOG.fine(\"exchange finished; ignoring late response attempt\");\n    return;\n}","typeGuard":"boolean responseStillOpen(LlmInferenceExchange x) { return !x.isFinished(); }","tryCatchPattern":"try {\n    exchange.startResponse(status, text, headers);\n} catch (IOException e) {\n    LOG.fine(\"exchange already finished; ignoring late callback\");\n}","preventionTips":["Check finished state in deferred/async error callbacks before touching the exchange.","Create a fresh exchange per request attempt; never reuse finished exchanges.","Cancel scheduled timeout/error handlers when the exchange completes normally."],"tags":["http","lifecycle","double-call"],"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-16T04:17:20.429Z"}