{"record":{"id":"bf64e4dc8f668308","repo":"github/copilot-sdk","slug":"llm-inference-response-used-after-rpc-connection-c-bf64e4","errorCode":null,"errorMessage":"LLM inference response used after RPC connection closed.","messagePattern":"LLM inference response used after RPC connection closed\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"dotnet/src/CopilotRequestHandler.cs","lineNumber":834,"sourceCode":"        }\n\n        if (!_started)\n        {\n            throw new InvalidOperationException(\"LLM inference response WriteAsync() called before StartAsync().\");\n        }\n\n        if (_finished)\n        {\n            throw new InvalidOperationException(\"LLM inference response WriteAsync() called after EndAsync()/ErrorAsync().\");\n        }\n\n        await ServerRpc()\n            .LlmInference.HttpResponseChunkAsync(RequestId, data, binary: binary, end: false)\n            .ConfigureAwait(false);\n    }\n\n    private ServerRpc ServerRpc() =>\n        _getServerRpc() ?? throw new InvalidOperationException(\"LLM inference response used after RPC connection closed.\");\n\n    private static Dictionary<string, IList<string>> ToWireHeaders(IReadOnlyDictionary<string, IReadOnlyList<string>>? headers)\n    {\n        var result = new Dictionary<string, IList<string>>(StringComparer.OrdinalIgnoreCase);\n        if (headers is null)\n        {\n            return result;\n        }\n\n        foreach (var (name, values) in headers)\n        {\n            result[name] = values as IList<string> ?? [.. values];\n        }\n\n        return result;\n    }\n\n    private struct BodyItem","sourceCodeStart":816,"sourceCodeEnd":852,"githubUrl":"https://github.com/github/copilot-sdk/blob/cd8cf15dc3f9e762615790aaed0a771a0f392755/dotnet/src/CopilotRequestHandler.cs#L816-L852","documentation":"LlmInferenceExchange sends each response chunk over the server RPC channel obtained via a lazy _getServerRpc accessor. When the RPC connection has been closed the accessor returns null, and this InvalidOperationException is thrown instead of attempting an RPC call on a dead channel.","triggerScenarios":"Writing or finishing a response chunk after the underlying RPC connection was closed/disposed — e.g. the runtime disconnected mid-response, or the exchange outlived the connection scope.","commonSituations":"Long-running LLM generation outlives a dropped connection; server shutdown closes the RPC while a response is still streaming; the exchange is cached and reused after reconnect.","solutions":["Discard/abort the exchange when the RPC connection closes rather than writing chunks","Track connection lifetime and cancel in-flight exchanges on disconnect","Re-create the exchange and retry over a fresh connection if the request is retriable","Check for exceptions during streaming and clean up exchange references in finally blocks"],"exampleFix":"// before\nawait exchange.WriteAsync(chunk); // may throw if RPC closed\n// after\nif (!connection.IsOpen) { await exchange.DisposeAsync(); return; }\nawait exchange.WriteAsync(chunk);","handlingStrategy":"type-guard","validationCode":"if (_getServerRpc() is null) { await exchange.DisposeAsync(); return; }","typeGuard":"bool RpcAlive(Func<ServerRpc?> getRpc) => getRpc() is not null;","tryCatchPattern":"try { await exchange.WriteAsync(chunk); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"RPC connection closed\")) { /* abort response, clean up exchange */ }","preventionTips":["Tie exchange lifetime to the connection lifetime","Cancel in-flight exchanges on disconnect events","Do not cache exchanges across reconnects"],"tags":["rpc","connection","llm","dotnet"],"backgroundTag":"broken-pipe","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"}