Fix ObjectDisposedException when disposing session after client.StopAsync()#481
Conversation
…sync() Make CopilotSession.DisposeAsync() idempotent and tolerant to already-disposed connections. This fixes the crash when 'await using var session' cleanup runs after client.StopAsync() has already disposed the connection. Changes: - Add _isDisposed flag with Interlocked.Exchange for thread-safe idempotency - Catch ObjectDisposedException and IOException during dispose (connection gone) - Add regression test to ClientTests Fixes #306
Cross-SDK Consistency Review ✅I've reviewed this PR for consistency across all four SDK implementations (Node.js/TypeScript, Python, Go, and .NET). SummaryThis PR fixes a .NET-specific issue where disposing a session after stopping the client throws
Cross-SDK AnalysisAfter reviewing the equivalent code in other SDKs, I found that this same issue exists in Node.js, Python, and Go, but they lack similar defensive handling: Node.js/TypeScript (
Python (
Go (
RecommendationConsider applying the same defensive pattern to the other SDKs:
This would ensure consistent, robust behavior across all SDK implementations, especially for users using automatic cleanup patterns ( Verdict✅ This PR is good to merge. While it highlights a cross-SDK inconsistency, the fix is appropriate for .NET and doesn't introduce new inconsistencies. The issue in other SDKs should be addressed separately.
|
There was a problem hiding this comment.
Pull request overview
This PR fixes a crash when disposing a CopilotSession after the client has already been stopped, by making CopilotSession.DisposeAsync() idempotent and tolerant to disposed connections. The fix addresses issue #306 where calling client.StopAsync() before a session's await using cleanup would cause an ObjectDisposedException.
Changes:
- Added idempotency to
DisposeAsync()using thread-safeInterlocked.Exchangepattern with_isDisposedflag - Added exception handling to gracefully handle
ObjectDisposedExceptionandIOExceptionduring disposal - Added regression test to verify the fix
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| dotnet/src/Session.cs | Implemented idempotent disposal with thread-safe flag and exception handling to prevent crashes when disposing after client shutdown |
| dotnet/test/ClientTests.cs | Added regression test verifying session disposal succeeds after client.StopAsync() without throwing exceptions |
| catch (ObjectDisposedException) | ||
| { | ||
| // Connection was already disposed (e.g., client.StopAsync() was called first) | ||
| } | ||
| catch (IOException) | ||
| { | ||
| // Connection is broken or closed | ||
| } |
There was a problem hiding this comment.
Consider catching all exceptions here rather than specific types. Dispose methods should be resilient and never throw exceptions as per IAsyncDisposable best practices. While ObjectDisposedException and IOException cover the expected cases, other unexpected exceptions (e.g., from custom JsonRpc implementations) could still propagate. Consider using a catch-all:
catch (Exception)
{
// Suppress all exceptions during disposal
}This matches the pattern used in CopilotClient.CleanupConnectionAsync which catches all exceptions during cleanup.
| catch (ObjectDisposedException) | |
| { | |
| // Connection was already disposed (e.g., client.StopAsync() was called first) | |
| } | |
| catch (IOException) | |
| { | |
| // Connection is broken or closed | |
| } | |
| catch (Exception) | |
| { | |
| // Suppress all exceptions during disposal (connection may already be closed or broken) | |
| } |
Summary
Cherry-picked from #371 with merge conflicts resolved. Closes #306.
Original author: @parthtrivedi2492
When
await client.StopAsync()is called beforeawait using var sessiongoes out of scope,session.DisposeAsync()is called twice — the second call throwsObjectDisposedException.Solution
Make
CopilotSession.DisposeAsync()idempotent and tolerant to already-disposed connections:_isDisposedflag — Thread-safe usingInterlocked.ExchangeDisposeAsyncidempotent — Returns immediately if already disposedObjectDisposedExceptionandIOExceptionChanges
dotnet/src/Session.cs: Add idempotency and exception handling toDisposeAsync()dotnet/test/ClientTests.cs: Add regression testConflict resolution
Session.cs: Both this PR and main added fields at the same location — kept both_sessionRpc(from main's RPC codegen) and_isDisposedClientTests.cs: Adapted test to usenew CopilotClientOptions()since_cliPathwas removed on main