{"record":{"id":"edcfab04202a4ce2","repo":"github/copilot-sdk","slug":"llm-inference-response-startresponse-called-twic","errorCode":null,"errorMessage":"LLM inference response startResponse() called twice","messagePattern":"LLM inference response startResponse\\(\\) called twice","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"java/sdk/src/main/java/com/github/copilot/LlmInferenceExchange.java","lineNumber":182,"sourceCode":"            }\n        }\n    }\n\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","sourceCodeStart":164,"sourceCodeEnd":200,"githubUrl":"https://github.com/github/copilot-sdk/blob/cd8cf15dc3f9e762615790aaed0a771a0f392755/java/sdk/src/main/java/com/github/copilot/LlmInferenceExchange.java#L164-L200","documentation":"LlmInferenceExchange.startResponse() guards the response lifecycle with a lock; if startResponse() is invoked when the 'started' flag is already set it throws IOException('LLM inference response startResponse() called twice'). A response's start (status/headers) may only be emitted once per exchange, mirroring HTTP semantics.","triggerScenarios":"Calling startResponse() twice for the same exchange, typically when both streamResponse() and finalizeError() run for one request (e.g. an error occurs after headers were already sent and the error path tries to start a new response) (LlmInferenceExchange.java:182).","commonSituations":"Error-handling code that responds with an HTTP error after streaming has already begun; race between a handler's normal response path and a late failure handler; missing 'if started' checks in layered response wrappers.","solutions":["Ensure only one code path calls startResponse(): guard error responses with an 'already started' check and use a different error channel (e.g. body-level error) once streaming began.","Coordinate streamResponse and finalizeError so they are mutually exclusive (e.g. compareAndSet-style state before responding).","Catch this IOException in the error path and fall back to aborting the exchange instead of re-starting the response."],"exampleFix":"// before\nvoid onError(int status, String msg) {\n    exchange.startResponse(status, msg, Map.of()); // may already be started\n}\n// after\nvoid onError(int status, String msg) {\n    if (!exchange.isStarted()) {\n        exchange.startResponse(status, msg, Map.of());\n    }\n}","handlingStrategy":"type-guard","validationCode":"if (responseStarted) {\n    LOG.warning(\"skipping startResponse; headers already sent\");\n    return;\n}","typeGuard":"boolean canStartResponse(LlmInferenceExchange x) { return !x.isStarted() && !x.isFinished(); }","tryCatchPattern":"try {\n    exchange.startResponse(status, text, headers);\n} catch (IOException e) {\n    LOG.warning(\"response already started/finished: \" + e.getMessage());\n}","preventionTips":["Make streamResponse and finalizeError mutually exclusive with a shared state flag.","Never emit an HTTP error response after streaming has started; use a body-level error instead.","Route all response emissions through one method that checks the lifecycle flags first."],"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"}