Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 26, 2026

This PR ensures that StreamJsonRpc.RemoteInvocationException is not exposed through the public API by wrapping all RPC communication errors with standard IOException.

Changes Made

  • Wrapper Methods: Created InvokeRpcAsync helper methods in both Session.cs and Client.cs to centralize exception handling
  • Exception Handling: All RPC calls now use the wrapper methods which catch StreamJsonRpc.RemoteRpcException and throw IOException instead
  • Tests Updated: Changed test assertions from StreamJsonRpc.RemoteInvocationException to IOException
  • Documentation Updated: Updated README.md error handling examples to use IOException
  • Code Refactoring: Made Client.InvokeRpcAsync internal and Session.InvokeRpcAsync now delegates to it to avoid duplication
  • Cleanup: Removed unused using System.IO; from Types.cs

Implementation Details

The wrapper methods provide a single point of exception translation:

  • Client.cs: Internal static method that accepts a JsonRpc parameter and catches RemoteRpcException
  • Session.cs: Instance method that delegates to Client.InvokeRpcAsync with the session's _rpc field

This approach eliminates ~115 lines of repetitive try-catch blocks while ensuring StreamJsonRpc remains an implementation detail that doesn't leak through the public API surface.

Testing

  • ✅ Code builds successfully with 0 warnings
  • ✅ Code formatting passes validation
  • ✅ No security vulnerabilities detected
  • ✅ All tests updated to use IOException
Original prompt

This section details on the original issue you should resolve

<issue_title>StreamJsonRpc use is being surfaced via exceptions</issue_title>
<issue_description>StreamJsonRpc is supposed to be an implementation detail of the .NET implementation. But its use is surfacing via StreamJsonRpc.RemoteInvocationException exceptions that emerge, and these are being documented and tested as part of its contract. Such exceptions should be caught and replaced either by an IOException, or if it's really important to call these out as JSON-RPC issues, a custom GH copilot exception type that surfaces the relevant information.

cc: @SteveSandersonMS </issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits January 26, 2026 15:12
Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>
Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix StreamJsonRpc exceptions in implementation Hide StreamJsonRpc implementation detail behind CopilotRpcException Jan 26, 2026
catch (StreamJsonRpc.RemoteInvocationException ex)
{
throw new CopilotRpcException($"Failed to get messages: {ex.Message}", ex);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Don't wrap more lines in this try/catch than you have to. It's enough only to wrap the InvokeWithCancellationAsync call.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 81d8dec. Now only the InvokeWithCancellationAsync calls are wrapped in try-catch blocks, with processing moved outside.

/// <remarks>
/// This exception wraps underlying communication errors to provide a stable API surface
/// that doesn't expose implementation details of the JSON-RPC protocol.
/// </remarks>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make these XML docs less specific about implementation details. No need to mention JSON-RPC specifically. Don't need to say why the exception is here (i.e. the stable API reason).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified in f46b3d5. Removed JSON-RPC mentions and the remarks section explaining the API design rationale.

@SteveSandersonMS
Copy link
Contributor

@stephentoub This looks reasonable to me. Is it the sort of thing you had in mind?

Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>
catch (StreamJsonRpc.RemoteInvocationException ex)
{
throw new CopilotRpcException($"Failed to destroy session: {ex.Message}", ex);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be cleaner / more maintainable to have a custom InvokeWithCancellationAsync wrapper that does the catch/throw and then all the call sites would just use that wrapper?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Please do this. Also we can just use IOException - no need for a custom CopilotRpcException type.

@stephentoub
Copy link
Contributor

Is it the sort of thing you had in mind?

If the ability to differentiate these exceptions by type is important (or if you want to expose additional strongly-typed data on the exception), yes.

Otherwise, it could just use the base IOException and not have a custom exception type.

Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>
@SteveSandersonMS
Copy link
Contributor

@copilot Address this feedback:

It would be cleaner / more maintainable to have a custom InvokeWithCancellationAsync wrapper that does the catch/throw and then all the call sites would just use that wrapper

do this. Also we can just use IOException - no need for a custom CopilotRpcException type.

@github-actions
Copy link

Cross-SDK Consistency Review

I've analyzed error handling patterns across all four SDK implementations and found partial consistency issues with this PR.

Current State

Good: Custom Exception Type Alignment

All SDKs except Node.js now use custom exception types for RPC errors:

  • Python: JsonRpcError
  • Go: JSONRPCError
  • .NET: CopilotRpcException (added in this PR)
  • Node.js: Uses generic Error class

This PR brings .NET into alignment with Python and Go.

⚠️ Concern: Structured Error Data Loss

The Python and Go SDKs preserve structured error information from JSON-RPC error responses:

Python (JsonRpcError):

class JsonRpcError(Exception):
    def __init__(self, code: int, message: str, data: Any = None):
        self.code = code      # RPC error code
        self.message = message
        self.data = data      # Additional error context

Go (JSONRPCError):

type JSONRPCError struct {
    Code    int                    `json:"code"`
    Message string                 `json:"message"`
    Data    map[string]interface{} `json:"data,omitempty"`
}

However, the .NET implementation only preserves the message string:

catch (StreamJsonRpc.RemoteInvocationException ex)
{
    throw new CopilotRpcException($"Failed to send message: {ex.Message}", ex);
}

Recommendation

Consider enhancing CopilotRpcException to preserve structured error information like Python/Go:

public class CopilotRpcException : IOException
{
    public int? ErrorCode { get; }
    public object? ErrorData { get; }
    
    public CopilotRpcException(string message, int? errorCode = null, object? errorData = null) : base(message)
    {
        ErrorCode = errorCode;
        ErrorData = errorData;
    }
}

This would allow .NET consumers to programmatically handle specific error types (e.g., authentication failures, session not found) consistently with Python and Go SDKs.

Impact

  • Current implementation: Functional, but consumers lose access to error codes/data that Python and Go users can access
  • If unaddressed: Cross-SDK applications will need different error handling strategies per language
  • Node.js: Should eventually adopt a similar custom exception pattern for full consistency

Verdict: This PR is a good step forward. The suggestion above is for future enhancement to achieve full parity with Python/Go error handling.

AI generated by SDK Consistency Review Agent

Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 26, 2026

@copilot Address this feedback:

It would be cleaner / more maintainable to have a custom InvokeWithCancellationAsync wrapper that does the catch/throw and then all the call sites would just use tha...

Implemented in 63b974a. Created InvokeRpcAsync wrapper methods in both Session and Client classes to centralize exception handling. Replaced CopilotRpcException with standard IOException. All RPC call sites now use the wrapper instead of manual try-catch blocks.

Copilot AI changed the title Hide StreamJsonRpc implementation detail behind CopilotRpcException Hide StreamJsonRpc implementation detail behind IOException Jan 26, 2026
@SteveSandersonMS SteveSandersonMS marked this pull request as ready for review January 26, 2026 17:13
@SteveSandersonMS SteveSandersonMS requested a review from a team as a code owner January 26, 2026 17:13
Copilot AI review requested due to automatic review settings January 26, 2026 17:13
@github-actions
Copy link

SDK Consistency Review: Error Handling Patterns

I've reviewed this PR for cross-SDK consistency. The changes hide the StreamJsonRpc.RemoteInvocationException implementation detail behind standard IOException, which improves the .NET SDK's public API surface.

Consistency Analysis

Current Error Handling Across SDKs:

SDK RPC Error Type Implementation Exposed?
Node.js Generic Error ✗ Hidden (aligned with this PR)
Python JsonRpcError ✓ Exposed (custom exception with code, message, data fields)
Go JSONRPCError ✓ Exposed (custom struct with Code, Message, Data fields)
.NET (after this PR) IOException ✗ Hidden (aligned with this PR)

Observations

✅ Good: This PR aligns the .NET SDK with Node.js by hiding JSON-RPC implementation details and using standard library exception types.

⚠️ Divergence: Python and Go SDKs currently expose JSON-RPC-specific error types that provide detailed error codes and messages. This creates two distinct error handling patterns:

  1. Abstracted approach (Node.js, .NET): Generic error types, simpler API, less diagnostic information
  2. Explicit approach (Python, Go): JSON-RPC-specific types, more detailed error information for debugging

Recommendation

This is not a blocking issue - the divergence reflects different language idioms:

  • JavaScript/C# developers expect generic error types
  • Python/Go developers often expect explicit error types with structured data

However, for future consideration: The team may want to document this intentional design divergence in the SDK design guidelines, or consider whether all SDKs should converge on one approach:

  • Option A: All SDKs hide RPC details (requires Python/Go to wrap errors like this PR)
  • Option B: All SDKs expose RPC details (would require .NET/Node.js to expose custom types)

No action required for this PR - the changes maintain .NET SDK quality and follow reasonable C# conventions. ✅

AI generated by SDK Consistency Review Agent

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses issue #201 by hiding the StreamJsonRpc implementation detail from the public API surface. Previously, StreamJsonRpc.RemoteInvocationException was leaking through to API consumers. The PR wraps all RPC communication errors with standard IOException to maintain abstraction boundaries.

Changes:

  • Created wrapper methods InvokeRpcAsync in both Session.cs and Client.cs to centralize exception handling
  • Updated all RPC invocation call sites to use the wrapper methods
  • Updated test assertions and documentation examples to expect IOException instead of StreamJsonRpc.RemoteInvocationException

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
dotnet/test/SessionTests.cs Updated test assertions to expect IOException instead of RemoteInvocationException
dotnet/src/Types.cs Added System.IO using statement (unnecessary - handled by implicit usings)
dotnet/src/Session.cs Added InvokeRpcAsync wrapper method and updated all RPC calls to use it
dotnet/src/Client.cs Added static InvokeRpcAsync wrapper method and updated all RPC calls to use it
dotnet/README.md Updated error handling documentation example to show IOException
Comments suppressed due to low confidence (1)

dotnet/src/Session.cs:101

  • The XML documentation should include an exception tag for IOException to inform API consumers that this method can throw IOException when communication errors occur with the Copilot CLI. Add: /// &lt;exception cref="IOException"&gt;Thrown when communication with the Copilot CLI fails.&lt;/exception&gt;
    /// <summary>
    /// Sends a message to the Copilot session and waits for the response.
    /// </summary>
    /// <param name="options">Options for the message to be sent, including the prompt and optional attachments.</param>
    /// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
    /// <returns>A task that resolves with the ID of the response message, which can be used to correlate events.</returns>
    /// <exception cref="InvalidOperationException">Thrown if the session has been disposed.</exception>

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

{
return await _rpc.InvokeWithCancellationAsync<T>(method, args, cancellationToken);
}
catch (StreamJsonRpc.RemoteInvocationException ex)
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider catching a broader set of StreamJsonRpc exceptions beyond just RemoteInvocationException. Other exceptions like ConnectionLostException, RemoteMethodNotFoundException, or the base RemoteRpcException could also be thrown during RPC operations and would currently leak through the abstraction. Consider catching the base RemoteRpcException type or handling these additional exception types to fully hide the StreamJsonRpc implementation details.

Suggested change
catch (StreamJsonRpc.RemoteInvocationException ex)
catch (StreamJsonRpc.RemoteRpcException ex)

Copilot uses AI. Check for mistakes.
{
return await rpc.InvokeWithCancellationAsync<T>(method, args, cancellationToken);
}
catch (StreamJsonRpc.RemoteInvocationException ex)
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider catching a broader set of StreamJsonRpc exceptions beyond just RemoteInvocationException. Other exceptions like ConnectionLostException, RemoteMethodNotFoundException, or the base RemoteRpcException could also be thrown during RPC operations and would currently leak through the abstraction. Consider catching the base RemoteRpcException type or handling these additional exception types to fully hide the StreamJsonRpc implementation details.

Suggested change
catch (StreamJsonRpc.RemoteInvocationException ex)
catch (StreamJsonRpc.RemoteRpcException ex)

Copilot uses AI. Check for mistakes.
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

using System.IO;
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The using System.IO; directive is unnecessary in this file since IOException is not used anywhere in Types.cs. This import should be removed. Note that IOException is used in Session.cs and Client.cs, but those files have implicit usings enabled via the project's ImplicitUsings setting, making System.IO automatically available.

Suggested change
using System.IO;

Copilot uses AI. Check for mistakes.
friggeri
friggeri previously approved these changes Jan 26, 2026
WorkspacePath = workspacePath;
}

private async Task<T> InvokeRpcAsync<T>(string method, object?[]? args, CancellationToken cancellationToken)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this method is identical to Client.InvokeRpcAsync, except that Client.InvokeRpcAsync accepts a JsonRpc parameter and this method bakes in the _rpc reference. If Client.InvokeRpcAsync were internal instead, this method could just be:

private Task<T> InvokeRpcAsync<T>(string method, object?[]? args, CancellationToken cancellationToken) =>
    Client.InvokeRpcAsync(_rpc, method, args, cancellationToken);

and avoid the duplication.

@SteveSandersonMS
Copy link
Contributor

@copilot Address the following review comments only:

  • Catch RemoteRpcException instead of RemoteInvocationException in both Session.cs and Client.cs
  • Remove any unnecessary using from Types.cs
  • The following about InvokeRpcAsync in Session.cs:

nit: this method is identical to Client.InvokeRpcAsync, except that Client.InvokeRpcAsync accepts a JsonRpc parameter and this method bakes in the _rpc reference. If Client.InvokeRpcAsync were internal instead, this method could just be:

private Task<T> InvokeRpcAsync<T>(string method, object?[]? args, CancellationToken cancellationToken) =>
    Client.InvokeRpcAsync(_rpc, method, args, cancellationToken);

and avoid the duplication.

…sed using

Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 27, 2026

@copilot Address the following review comments only:

  • Catch RemoteRpcException instead of RemoteInvocationException in both Session.cs and Client.cs
    ...

Addressed in bad23c1:

  • Changed catch blocks to catch RemoteRpcException instead of RemoteInvocationException in both Client.cs and Session.cs
  • Removed unused using System.IO; from Types.cs
  • Made Client.InvokeRpcAsync internal and refactored Session.InvokeRpcAsync to delegate to it, eliminating duplication

@SteveSandersonMS SteveSandersonMS added this pull request to the merge queue Jan 27, 2026
Merged via the queue into main with commit b5826ca Jan 27, 2026
25 checks passed
@SteveSandersonMS SteveSandersonMS deleted the copilot/fix-streamjsonrpc-exceptions branch January 27, 2026 10:17
@github-actions
Copy link

Cross-SDK Consistency Review ✅

I've reviewed this PR for consistency across all four SDK implementations (Node.js, Python, Go, .NET). Here are my findings:

Summary

This PR introduces a deliberate divergence in error handling philosophy between the .NET SDK and the other three SDKs. While not a bug, teams should be aware of this difference when documenting and supporting the SDKs.


Error Handling Comparison

SDK RPC Library User-Facing Errors Abstraction Level
Node.js Custom JSON-RPC Generic Error Low (raw errors)
Python Custom JSON-RPC Custom JsonRpcError Medium (custom exception)
Go Custom JSON-RPC Custom JSONRPCError Medium (custom error type)
.NET StreamJsonRpc (external) IOException (stdlib) High (wrapped)

Key Differences

  1. Implementation Library

    • Node.js/Python/Go: All use custom JSON-RPC implementations (no external dependencies)
    • .NET: Uses StreamJsonRpc external library (this PR hides this implementation detail)
  2. Error Exposure Philosophy

    • Node.js/Python/Go: Expose custom RPC error types (JsonRpcError, JSONRPCError) that preserve JSON-RPC semantic information (error codes, data, etc.)
    • .NET (this PR): Wraps all RPC errors in standard library IOException, making implementation details opaque but sacrificing RPC-specific error metadata
  3. Error Information

    • Python/Go: Custom error types expose code, message, and data fields from JSON-RPC error responses
    • .NET: Only exposes generic message string via IOException.Message (original exception available via InnerException)

Consistency Considerations

This PR is acceptable because:

  1. The .NET SDK is the only one using an external library (StreamJsonRpc), so wrapping it makes sense to avoid leaking implementation details
  2. The change improves the .NET SDK's public API by using framework-standard exceptions
  3. Language idioms differ: .NET strongly emphasizes standard BCL exceptions

⚠️ Potential cross-SDK impact:

  1. Documentation inconsistency: Error handling guides will need to differ across languages

    • Python/Go docs can reference JsonRpcError/JSONRPCError with specific error codes
    • .NET docs must use generic IOException and recommend checking InnerException for details
  2. Diagnostic difficulty: .NET users lose direct access to JSON-RPC error codes/data unless they inspect InnerException

  3. Future consideration: If Node.js/Python/Go ever switch to external libraries, they may face the same wrapping decision


Recommendations

  1. Documentation: Ensure error handling examples in each SDK's README reflect the correct exception type (already done in this PR for .NET)
  2. Consider: If preserving JSON-RPC error metadata is important for .NET users, consider a custom exception type (e.g., CopilotRpcException : IOException) that exposes structured error info
  3. No immediate action needed: The current approach is valid given the different implementation strategies

Verdict

No blocking issues found. This PR maintains internal consistency within the .NET SDK and appropriately handles the fact that it uses a different RPC implementation than the other SDKs. The divergence in error handling is intentional and justified by the use of an external library.

The changes are well-implemented with proper test updates and documentation updates included. ✨

AI generated by SDK Consistency Review Agent

SilentDemonSD pushed a commit to SilentDemonSD/copilot-sdk that referenced this pull request Jan 27, 2026
* Initial plan

* Wrap StreamJsonRpc exceptions with CopilotRpcException

Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>

* Add explicit using System.IO for IOException

Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>

* Only wrap RPC invocation calls in try-catch blocks

Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>

* Remove implementation details from CopilotRpcException docs

Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>

* Replace CopilotRpcException with IOException and add RPC wrapper methods

Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>

* Catch RemoteRpcException, refactor Session.InvokeRpcAsync, remove unused using

Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StreamJsonRpc use is being surfaced via exceptions

4 participants