github/copilot-sdk · error · IOException

LLM inference response writeResponse() called after…

Error message

LLM inference response writeResponse() called after endResponse()/errorResponse()

What it means

This IOException is thrown when writeResponseText/writeResponseBinary is called after the exchange has already been finished via endResponse() or errorResponse(). Once finished, the response stream is complete and no further chunks can be sent over the RPC connection; the library rejects the write to keep the protocol consistent.

Solutions

  1. Reorder code so all chunks are written before endResponse()/errorResponse()
  2. Remove any writes in finally/cleanup blocks after the response is finished
  3. Track completion state in your handler and skip writes once endResponse has been called
  4. Catch this IOException and log rather than retrying — the exchange cannot be reopened

Example fix

// before
exchange.writeResponseText("done");
exchange.endResponse();
// after
exchange.writeResponseText("done");
exchange.endResponse(); // no writes after this point
Defensive patterns

Strategy: validation

Validate before calling

if (!responseFinished) {
    exchange.writeResponseText(chunk);
}

Try / catch

try {
    exchange.writeResponseText(chunk);
} catch (IOException e) {
    if (String.valueOf(e.getMessage()).contains("after endResponse")) {
        LOG.warning("skipped write after response finished");
        return;
    }
    throw e;
}

Prevention

When it happens

Trigger: Calling writeResponseText or writeResponseBinary after endResponse() or errorResponse() returned — e.g., writing a final summary chunk after closing the stream, or an async callback firing after the response was finalized.

Common situations: Async/continuation code that writes a trailing chunk after endResponse; duplicated finalize logic where endResponse is called then more writes happen in a finally block; retried handlers re-invoking write after the first attempt completed the response.

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/7536eabec4390b5b. Report an issue: GitHub.

Appendix: source

Thrown at java/sdk/src/main/java/com/github/copilot/LlmInferenceExchange.java:238

                return;
            }
            finished = true;
        }
        var error = new LlmInferenceHttpResponseChunkError(message, code);
        var params = new LlmInferenceHttpResponseChunkParams(requestId, "", null, Boolean.TRUE, error);
        join(api().httpResponseChunk(params));
    }

    private void writeChunk(String data, boolean binary) throws IOException {
        synchronized (lock) {
            if (cancelled) {
                throw new IOException("LLM inference request was cancelled by the runtime");
            }
            if (!started) {
                throw new IOException("LLM inference response writeResponse() called before startResponse()");
            }
            if (finished) {
                throw new IOException(
                        "LLM inference response writeResponse() called after endResponse()/errorResponse()");
            }
        }
        var params = new LlmInferenceHttpResponseChunkParams(requestId, data, binary ? Boolean.TRUE : null,
                Boolean.FALSE, null);
        join(api().httpResponseChunk(params));
    }

    private ServerLlmInferenceApi api() throws IOException {
        ServerLlmInferenceApi api = rpcSupplier.get();
        if (api == null) {
            throw new IOException("LLM inference response used after RPC connection closed");
        }
        return api;
    }

    private static <T> T join(CompletableFuture<T> future) throws IOException {
        try {

View on GitHub (pinned to cd8cf15dc3)