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

  1. Ensure exactly one terminal call (endResponse or errorResponse) per request and no emit calls after it.
  2. Structure the handler so all response logic runs before finalization; do not resume emitting after catch blocks that already responded.
  3. Track a local sent/finished flag in wrapper code before invoking startResponse.
  4. 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

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


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)