github/copilot-sdk · error
Copilot request response already finished.
Error message
Copilot request response already finished.
What it means
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.
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.
Example fix
// before
try { await doWork(h); } catch (e) { await h.errorResponse(500, String(e)); }
await h.startResponse({ status: 200 }); // throws: already finished
// after
try { await doWork(h); } catch (e) { await h.errorResponse(500, String(e)); return; }
await h.startResponse({ status: 200 }); Defensive patterns
Strategy: validation
Validate before calling
let finished = false;
async function finish(h) {
if (finished) return;
finished = true;
await h.endResponse();
} Try / catch
try {
await handler.startResponse({ status: 200 });
} catch (e) {
if (e instanceof Error && e.message.includes('already finished')) return; // response closed
throw e;
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Copilot request response write() called after end()/error().
- Copilot request response start() called twice.
- Copilot request response write() called before start().
- Copilot request response used after RPC connection closed.
- CLI process not started
AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09).
Data as JSON: /api/errors/c828e2dda273d74b.
Report an issue: GitHub.
Appendix: source
Thrown at nodejs/src/copilotRequestHandler.ts:582
this.#drained = true;
return { value: undefined, done: true };
}
return { value: item.chunk ?? new Uint8Array(), done: false };
},
}),
};
}
// --- Response emit (driven by the handler). Strict state machine: ---
// startResponse once -> 0..N writeResponse -> exactly one of
// endResponse / errorResponse.
async startResponse(init: ResponseInit): Promise<void> {
if (this.#started) {
throw new Error("Copilot request response start() called twice.");
}
if (this.#finished) {
throw new Error("Copilot request response already finished.");
}
this.#started = true;
await this.#rpc().llmInference.httpResponseStart({
requestId: this.requestId,
status: init.status,
statusText: init.statusText,
headers: init.headers ?? {},
});
}
async writeResponse(data: string | Uint8Array): Promise<void> {
if (this.#cancelled) {
throw new Error("Copilot request was cancelled by the runtime.");
}
if (!this.#started) {
throw new Error("Copilot request response write() called before start().");
}
if (this.#finished) {View on GitHub (pinned to cd8cf15dc3)