github/copilot-sdk · error · IOException
LLM inference response startResponse() called twice
Error message
LLM inference response startResponse() called twice
What it means
LlmInferenceExchange.startResponse() guards the response lifecycle with a lock; if startResponse() is invoked when the 'started' flag is already set it throws IOException('LLM inference response startResponse() called twice'). A response's start (status/headers) may only be emitted once per exchange, mirroring HTTP semantics.
Solutions
- Ensure only one code path calls startResponse(): guard error responses with an 'already started' check and use a different error channel (e.g. body-level error) once streaming began.
- Coordinate streamResponse and finalizeError so they are mutually exclusive (e.g. compareAndSet-style state before responding).
- Catch this IOException in the error path and fall back to aborting the exchange instead of re-starting the response.
Example fix
// before
void onError(int status, String msg) {
exchange.startResponse(status, msg, Map.of()); // may already be started
}
// after
void onError(int status, String msg) {
if (!exchange.isStarted()) {
exchange.startResponse(status, msg, Map.of());
}
} Defensive patterns
Strategy: type-guard
Validate before calling
if (responseStarted) {
LOG.warning("skipping startResponse; headers already sent");
return;
} Type guard
boolean canStartResponse(LlmInferenceExchange x) { return !x.isStarted() && !x.isFinished(); } Try / catch
try {
exchange.startResponse(status, text, headers);
} catch (IOException e) {
LOG.warning("response already started/finished: " + e.getMessage());
} Prevention
- Make streamResponse and finalizeError mutually exclusive with a shared state flag.
- Never emit an HTTP error response after streaming has started; use a body-level error instead.
- Route all response emissions through one method that checks the lifecycle flags first.
When it happens
Trigger: Calling startResponse() twice for the same exchange, typically when both streamResponse() and finalizeError() run for one request (e.g. an error occurs after headers were already sent and the error path tries to start a new response) (LlmInferenceExchange.java:182).
Common situations: Error-handling code that responds with an HTTP error after streaming has already begun; race between a handler's normal response path and a late failure handler; missing 'if started' checks in layered response wrappers.
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
- LLM inference response already finished
- CLI process not started
- CLI child process was unexpectedly started in parent…
- Server port not available
- Session not found
AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09).
Data as JSON: /api/errors/edcfab04202a4ce2.
Report an issue: GitHub.
Appendix: source
Thrown at java/sdk/src/main/java/com/github/copilot/LlmInferenceExchange.java:182
}
}
}
byte[] drainBody() throws InterruptedException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
BodyFrame frame;
while ((frame = readFrame()) != null) {
out.writeBytes(frame.data());
}
return out.toByteArray();
}
// --- Response emit (driven by the handler) ---
void startResponse(int status, String statusText, Map<String, List<String>> headers) throws IOException {
synchronized (lock) {
if (started) {
throw new IOException("LLM inference response startResponse() called twice");
}
if (finished) {
throw new IOException("LLM inference response already finished");
}
started = true;
}
var params = new LlmInferenceHttpResponseStartParams(requestId, (long) status, statusText, headers);
join(api().httpResponseStart(params));
}
void writeResponseText(String text) throws IOException {
writeChunk(text, false);
}
void writeResponseBinary(byte[] data) throws IOException {
writeChunk(Base64.getEncoder().encodeToString(data), true);
}
View on GitHub (pinned to cd8cf15dc3)