github/copilot-sdk · error
Copilot request response write() called before start().
Error message
Copilot request response write() called before start().
What it means
The response state machine requires startResponse() to run before any writeResponse(). Writes map to httpResponseChunk RPC calls which only exist after httpResponseStart established status/headers. Writing before start is an out-of-order lifecycle call, so the library throws.
Solutions
- Always call await handler.startResponse({ status, headers }) once before the first writeResponse().
- Prefer streamResponse() or finalize(), which manage the start/write/end sequence for you.
- Audit error paths that might skip startResponse while later code still writes.
- Check for swallowed rejections from startResponse that leave #started false while the write path proceeds.
Example fix
// before
await handler.writeResponse('hello'); // throws: not started
// after
await handler.startResponse({ status: 200 });
await handler.writeResponse('hello'); Defensive patterns
Strategy: validation
Validate before calling
let started = false;
async function safeWrite(h, data) {
if (!started) { await h.startResponse({ status: 200 }); started = true; }
await h.writeResponse(data);
} Try / catch
try {
await handler.writeResponse(chunk);
} catch (e) {
if (e instanceof Error && e.message.includes('called before start()')) {
await handler.startResponse({ status: 200 });
await handler.writeResponse(chunk);
return;
}
throw e;
} Prevention
- Always call startResponse() before the first writeResponse().
- Prefer streamResponse()/finalize() to avoid manual ordering.
- Audit paths that skip startResponse conditionally.
- Don't ignore startResponse rejections — a swallowed failure leaves the state machine unstarted.
When it happens
Trigger: Calling writeResponse() (or streamResponse internals) without a prior successful startResponse() on the same handler — e.g. skipping start when writing the first chunk manually.
Common situations: Handler authors who assume writeResponse implicitly starts the response; refactorings that removed the start call; conditional code paths where startResponse was skipped due to an earlier error.
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 start() called twice.
- Copilot request response already finished.
- Copilot request response write() called after end()/error().
- Copilot request response used after RPC connection closed.
- Copilot request response start() called twice.
AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09).
Data as JSON: /api/errors/7bc62d218fe179e1.
Report an issue: GitHub.
Appendix: source
Thrown at nodejs/src/copilotRequestHandler.ts:598
}
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) {
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;View on GitHub (pinned to cd8cf15dc3)