{"record":{"id":"c828e2dda273d74b","repo":"github/copilot-sdk","slug":"copilot-request-response-already-finished","errorCode":null,"errorMessage":"Copilot request response already finished.","messagePattern":"Copilot request response already finished\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nodejs/src/copilotRequestHandler.ts","lineNumber":582,"sourceCode":"                        this.#drained = true;\n                        return { value: undefined, done: true };\n                    }\n                    return { value: item.chunk ?? new Uint8Array(), done: false };\n                },\n            }),\n        };\n    }\n\n    // --- Response emit (driven by the handler). Strict state machine: ---\n    // startResponse once -> 0..N writeResponse -> exactly one of\n    // endResponse / errorResponse.\n\n    async startResponse(init: ResponseInit): Promise<void> {\n        if (this.#started) {\n            throw new Error(\"Copilot request response start() called twice.\");\n        }\n        if (this.#finished) {\n            throw new Error(\"Copilot request response already finished.\");\n        }\n        this.#started = true;\n        await this.#rpc().llmInference.httpResponseStart({\n            requestId: this.requestId,\n            status: init.status,\n            statusText: init.statusText,\n            headers: init.headers ?? {},\n        });\n    }\n\n    async writeResponse(data: string | Uint8Array): Promise<void> {\n        if (this.#cancelled) {\n            throw new Error(\"Copilot request was cancelled by the runtime.\");\n        }\n        if (!this.#started) {\n            throw new Error(\"Copilot request response write() called before start().\");\n        }\n        if (this.#finished) {","sourceCodeStart":564,"sourceCodeEnd":600,"githubUrl":"https://github.com/github/copilot-sdk/blob/cd8cf15dc3f9e762615790aaed0a771a0f392755/nodejs/src/copilotRequestHandler.ts#L564-L600","documentation":"Once endResponse() or errorResponse() has completed, the response for the request is finished and the state machine is closed. Any later startResponse() call would attempt httpResponseStart on a completed response, so the library rejects it. This guards against emitting more than one terminal lifecycle per request.","triggerScenarios":"Calling startResponse() after endResponse() or errorResponse() has already run — typically a retry after an error response, or a code path that finishes the response and then falls through into another emit routine (finalize/streamResponse).","commonSituations":"Writing an error response and then trying to send a normal response in a catch block; shared wrapper code that finalizes the response and then the handler also tries to stream output.","solutions":["Ensure exactly one terminal call (endResponse or errorResponse) per request and no emit calls after it.","Structure the handler so all response logic runs before finalization; do not resume emitting after catch blocks that already responded.","Track a local sent/finished flag in wrapper code before invoking startResponse.","Return immediately after endResponse()/errorResponse() so later code cannot run."],"exampleFix":"// before\ntry { await doWork(h); } catch (e) { await h.errorResponse(500, String(e)); }\nawait h.startResponse({ status: 200 }); // throws: already finished\n// after\ntry { await doWork(h); } catch (e) { await h.errorResponse(500, String(e)); return; }\nawait h.startResponse({ status: 200 });","handlingStrategy":"validation","validationCode":"let finished = false;\nasync function finish(h) {\n  if (finished) return;\n  finished = true;\n  await h.endResponse();\n}","typeGuard":null,"tryCatchPattern":"try {\n  await handler.startResponse({ status: 200 });\n} catch (e) {\n  if (e instanceof Error && e.message.includes('already finished')) return; // response closed\n  throw e;\n}","preventionTips":["Call endResponse()/errorResponse() exactly once and return right after.","Ensure catch blocks that send an error response also stop normal response flow.","Track terminal state in a local flag for shared wrapper code.","Do not retry emissions after an error response."],"tags":["state-machine","response","lifecycle"],"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"}