{"record":{"id":"80a3394e00f8fbca","repo":"github/copilot-sdk","slug":"llm-inference-response-startasync-called-twice","errorCode":null,"errorMessage":"LLM inference response StartAsync() called twice.","messagePattern":"LLM inference response StartAsync\\(\\) called twice\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"dotnet/src/CopilotRequestHandler.cs","lineNumber":758,"sourceCode":"                }\n\n                if (item.Chunk is { Length: > 0 })\n                {\n                    yield return item.Chunk;\n                }\n            }\n        }\n    }\n\n    // --- Response emit (driven by the handler). Strict state machine: ---\n    // StartResponseAsync once -> zero or more WriteResponseAsync -> exactly one\n    // of EndResponseAsync / ErrorResponseAsync.\n\n    internal async Task StartResponseAsync(int status, string? statusText, IReadOnlyDictionary<string, IReadOnlyList<string>>? headers)\n    {\n        if (_started)\n        {\n            throw new InvalidOperationException(\"LLM inference response StartAsync() called twice.\");\n        }\n\n        if (_finished)\n        {\n            throw new InvalidOperationException(\"LLM inference response already finished.\");\n        }\n\n        _started = true;\n        await ServerRpc()\n            .LlmInference.HttpResponseStartAsync(RequestId, status, ToWireHeaders(headers), statusText)\n            .ConfigureAwait(false);\n    }\n\n    internal Task WriteResponseAsync(ReadOnlyMemory<byte> data) =>\n        WriteChunkAsync(Convert.ToBase64String(data.ToArray()), binary: true);\n\n    internal Task WriteResponseAsync(string text)\n    {","sourceCodeStart":740,"sourceCodeEnd":776,"githubUrl":"https://github.com/github/copilot-sdk/blob/cd8cf15dc3f9e762615790aaed0a771a0f392755/dotnet/src/CopilotRequestHandler.cs#L740-L776","documentation":"LlmInferenceExchange.StartResponseAsync writes the HTTP-style start-of-response (status/headers) exactly once per request. This InvalidOperationException guards against calling the response start twice, which would corrupt the single-response RPC protocol.","triggerScenarios":"Calling StartAsync (or the code path that invokes StartResponseAsync) twice on the same LlmInferenceExchange — e.g. double invocation of a completion callback, or retry logic re-sending the response start on the same exchange.","commonSituations":"Middleware layered so both a handler and a fallback try to start the response; an error path starts the response and then a catch block starts it again; framework code retries StartAsync after a partial failure.","solutions":["Guard response-start logic with a local started flag or only call StartAsync once per exchange","Move retry logic so a NEW exchange is used for each attempt instead of reusing one","Ensure error paths use ErrorResponseAsync rather than another StartAsync"],"exampleFix":"// before\nawait exchange.StartAsync(200, \"OK\", headers);\nawait exchange.StartAsync(200, \"OK\", headers); // throws\n// after\nif (!_responseStarted) { await exchange.StartAsync(200, \"OK\", headers); _responseStarted = true; }","handlingStrategy":"type-guard","validationCode":"if (_started) return; // already started; skip duplicate StartAsync","typeGuard":"bool CanStart(LlmInferenceExchange ex) => !ex.Started && !ex.Finished;","tryCatchPattern":"try { await exchange.StartAsync(200, \"OK\", headers); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"called twice\")) { /* ignore duplicate start */ }","preventionTips":["Guard response start with a local flag","Retry with a fresh exchange, never reuse one","Use ErrorResponseAsync on error paths instead of re-starting"],"tags":["llm","protocol","state","dotnet"],"backgroundTag":"invalid-state-transition","analyzedSha":"cd8cf15dc3f9e762615790aaed0a771a0f392755","analyzedAt":"2026-09-09T18:32:31.973Z","contentChangedAt":"2026-09-09T18:32:31.973Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}