github/copilot-sdk · error
Copilot request response write() called after end()/error().
Error message
Copilot request response write() called after end()/error().
What it means
After endResponse() or errorResponse() runs, the response is terminally finished and no further httpResponseChunk writes are valid. writeResponse() checks the finished flag and throws to keep the response lifecycle one start, N writes, one end/error.
Solutions
- Stop writing once endResponse()/errorResponse() has been called; reorder so end is the last statement after the write loop.
- Use a local finished flag around the emit loop and check it before each write.
- Prefer streamResponse()/finalize() which encapsulate ordering and prevent post-end writes.
- On catching this error, treat the extra data as dropped and log at debug level.
Example fix
// before
await h.endResponse();
await h.writeResponse('trailer'); // throws
// after
await h.writeResponse('trailer');
await h.endResponse(); // end strictly last Defensive patterns
Strategy: validation
Validate before calling
let done = false;
async function safeWrite(h, data) {
if (done) return; // no writes after end/error
await h.writeResponse(data);
}
async function safeEnd(h) { if (!done) { done = true; await h.endResponse(); } } Try / catch
try {
await handler.writeResponse(trailer);
} catch (e) {
if (e instanceof Error && e.message.includes('called after end()/error()')) return; // drop late data
throw e;
} Prevention
- Make endResponse()/errorResponse() the final statement of the handler.
- Guard write loops with a local finished flag.
- Use streamResponse()/finalize() to encapsulate ordering.
- Verify finally blocks don't end the response while a write loop continues.
When it happens
Trigger: Calling writeResponse() after endResponse()/errorResponse() completed — e.g. flushing trailing data after ending, or a loop that keeps writing past a finally block that ends the response.
Common situations: Streams whose completion and end-of-response are handled in different places; catch/finally blocks that call endResponse() while the generator loop continues; double-sending a trailer.
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 already finished.
- 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/63d5d5f2da75cbf4.
Report an issue: GitHub.
Appendix: source
Thrown at nodejs/src/copilotRequestHandler.ts:601
}
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) {
throw new Error("Copilot request response write() called after end()/error().");
}
const isString = typeof data === "string";
await this.#rpc().llmInference.httpResponseChunk({
requestId: this.requestId,
data: isString ? data : Buffer.from(data).toString("base64"),
binary: !isString,
end: false,
});
}
async endResponse(): Promise<void> {
if (this.#finished) {
return;
}
this.#finished = true;
await this.#rpc().llmInference.httpResponseChunk({
requestId: this.requestId,
data: "",View on GitHub (pinned to cd8cf15dc3)