From 1ee8568205506a567e0550309c26a8b432e9ac70 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 25 May 2026 23:51:57 -0400 Subject: [PATCH 1/6] Preserve JSON-RPC error data in .NET Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/JsonRpc.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dotnet/src/JsonRpc.cs b/dotnet/src/JsonRpc.cs index df7170373..ff2fc7f46 100644 --- a/dotnet/src/JsonRpc.cs +++ b/dotnet/src/JsonRpc.cs @@ -440,7 +440,10 @@ private void HandleResponse(JsonElement message, JsonElement idProp) var errorCode = errorProp.TryGetProperty("code", out var codeProp) && codeProp.ValueKind == JsonValueKind.Number ? codeProp.GetInt32() : 0; - pending.TrySetException(new RemoteRpcException(errorMessage, errorCode)); + var errorData = errorProp.TryGetProperty("data", out var dataProp) + ? dataProp.Clone() + : (JsonElement?)null; + pending.TrySetException(new RemoteRpcException(errorMessage, errorCode, errorData)); } else if (message.TryGetProperty("result", out var resultProp)) { @@ -899,12 +902,14 @@ internal sealed class ConnectionLostException() : IOException("The JSON-RPC conn /// /// Thrown when the remote side returns a JSON-RPC error response. /// -internal sealed class RemoteRpcException(string message, int errorCode, Exception? innerException = null) : Exception(message, innerException) +internal sealed class RemoteRpcException(string message, int errorCode, JsonElement? errorData = null, Exception? innerException = null) : Exception(message, innerException) { /// JSON-RPC 2.0 reserved error code: requested method does not exist. public const int MethodNotFoundErrorCode = -32601; public int ErrorCode { get; } = errorCode; + + public JsonElement? ErrorData { get; } = errorData.HasValue ? errorData.Value.Clone() : null; } /// From fde8a1f60a56623b1d82964c61c4728d187527b5 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 26 May 2026 09:25:56 -0400 Subject: [PATCH 2/6] Address JSON-RPC error data review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/JsonRpc.cs | 2 +- dotnet/test/Unit/JsonRpcTests.cs | 67 ++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/dotnet/src/JsonRpc.cs b/dotnet/src/JsonRpc.cs index ff2fc7f46..bc35d52bf 100644 --- a/dotnet/src/JsonRpc.cs +++ b/dotnet/src/JsonRpc.cs @@ -441,7 +441,7 @@ private void HandleResponse(JsonElement message, JsonElement idProp) ? codeProp.GetInt32() : 0; var errorData = errorProp.TryGetProperty("data", out var dataProp) - ? dataProp.Clone() + ? dataProp : (JsonElement?)null; pending.TrySetException(new RemoteRpcException(errorMessage, errorCode, errorData)); } diff --git a/dotnet/test/Unit/JsonRpcTests.cs b/dotnet/test/Unit/JsonRpcTests.cs index 6c045c8eb..40d829728 100644 --- a/dotnet/test/Unit/JsonRpcTests.cs +++ b/dotnet/test/Unit/JsonRpcTests.cs @@ -78,6 +78,49 @@ static ValueTask HandleSingleObject(SingleObjectRequest re ValueTask.FromResult(new SingleObjectResponse { Value = request.Value }); } + [Fact] + public async Task JsonRpc_Preserves_Remote_Error_Data() + { + using var pair = JsonRpcReflectionPair.Create(); + + pair.Server.SetLocalRpcMethod( + "structuredError", + (Func>)((_, _, _) => throw CreateLocalRpcInvocationException( + -32603, + "No handler implemented for this canvas action", + CreateJsonElement("""{"code":"canvas_action_no_handler","message":"No handler implemented for this canvas action"}""")))); + pair.Server.SetLocalRpcMethod( + "nullErrorData", + (Func>)((_, _, _) => throw CreateLocalRpcInvocationException( + -32603, + "Null error data", + CreateJsonElement("null")))); + pair.Server.SetLocalRpcMethod( + "omittedErrorData", + (Func>)((_, _, _) => throw CreateLocalRpcInvocationException( + -32603, + "Omitted error data"))); + + pair.StartListening(); + + var structured = await Assert.ThrowsAnyAsync(() => + pair.Client.InvokeAsync("structuredError", ["invoke", 1])); + Assert.Equal(-32603, GetRemoteErrorCode(structured)); + + var data = GetRemoteErrorData(structured); + Assert.NotNull(data); + Assert.Equal("canvas_action_no_handler", data.Value.GetProperty("code").GetString()); + Assert.Equal("No handler implemented for this canvas action", data.Value.GetProperty("message").GetString()); + + var nullData = await Assert.ThrowsAnyAsync(() => + pair.Client.InvokeAsync("nullErrorData", ["invoke", 1])); + Assert.Equal(JsonValueKind.Null, GetRemoteErrorData(nullData)?.ValueKind); + + var omittedData = await Assert.ThrowsAnyAsync(() => + pair.Client.InvokeAsync("omittedErrorData", ["invoke", 1])); + Assert.Null(GetRemoteErrorData(omittedData)); + } + [Fact] public async Task JsonRpc_Cancels_And_Disposes_Pending_Requests() { @@ -100,6 +143,30 @@ private static int GetRemoteErrorCode(Exception exception) return (int)property.GetValue(exception)!; } + private static JsonElement? GetRemoteErrorData(Exception exception) + { + var property = exception.GetType().GetProperty("ErrorData", BindingFlags.Instance | BindingFlags.Public); + Assert.NotNull(property); + return property.GetValue(exception) is JsonElement value ? value : null; + } + + private static JsonElement CreateJsonElement(string json) + { + using var document = JsonDocument.Parse(json); + return document.RootElement.Clone(); + } + + private static Exception CreateLocalRpcInvocationException(int code, string message, JsonElement? data = null) + { + var type = typeof(CopilotClient).Assembly.GetType("GitHub.Copilot.LocalRpcInvocationException", throwOnError: true)!; + return (Exception)Activator.CreateInstance( + type, + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + binder: null, + args: [code, message, data], + culture: null)!; + } + private sealed class NamedParams { public string Name { get; set; } = string.Empty; From c3d5b1075e62b24e7005ac0e5cb6e60071c2a334 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 26 May 2026 09:39:06 -0400 Subject: [PATCH 3/6] Fix JsonRpc test nullability warning Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/test/Unit/JsonRpcTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dotnet/test/Unit/JsonRpcTests.cs b/dotnet/test/Unit/JsonRpcTests.cs index 40d829728..a2cc1159c 100644 --- a/dotnet/test/Unit/JsonRpcTests.cs +++ b/dotnet/test/Unit/JsonRpcTests.cs @@ -109,8 +109,9 @@ public async Task JsonRpc_Preserves_Remote_Error_Data() var data = GetRemoteErrorData(structured); Assert.NotNull(data); - Assert.Equal("canvas_action_no_handler", data.Value.GetProperty("code").GetString()); - Assert.Equal("No handler implemented for this canvas action", data.Value.GetProperty("message").GetString()); + var errorData = data.Value; + Assert.Equal("canvas_action_no_handler", errorData.GetProperty("code").GetString()); + Assert.Equal("No handler implemented for this canvas action", errorData.GetProperty("message").GetString()); var nullData = await Assert.ThrowsAnyAsync(() => pair.Client.InvokeAsync("nullErrorData", ["invoke", 1])); From 3b64c66334717ad4a19b1dd96c83178de8bba3fd Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 26 May 2026 11:11:22 -0400 Subject: [PATCH 4/6] Address JsonRpc test nullability feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/test/Unit/JsonRpcTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/test/Unit/JsonRpcTests.cs b/dotnet/test/Unit/JsonRpcTests.cs index a2cc1159c..273bdafb6 100644 --- a/dotnet/test/Unit/JsonRpcTests.cs +++ b/dotnet/test/Unit/JsonRpcTests.cs @@ -109,7 +109,7 @@ public async Task JsonRpc_Preserves_Remote_Error_Data() var data = GetRemoteErrorData(structured); Assert.NotNull(data); - var errorData = data.Value; + var errorData = data!.Value; Assert.Equal("canvas_action_no_handler", errorData.GetProperty("code").GetString()); Assert.Equal("No handler implemented for this canvas action", errorData.GetProperty("message").GetString()); From 97db9b7396c0150869a0e2a406722d1ca3bf63ce Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 26 May 2026 11:45:35 -0400 Subject: [PATCH 5/6] Revert JsonRpcTests null-forgiving change Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/test/Unit/JsonRpcTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/test/Unit/JsonRpcTests.cs b/dotnet/test/Unit/JsonRpcTests.cs index 273bdafb6..a2cc1159c 100644 --- a/dotnet/test/Unit/JsonRpcTests.cs +++ b/dotnet/test/Unit/JsonRpcTests.cs @@ -109,7 +109,7 @@ public async Task JsonRpc_Preserves_Remote_Error_Data() var data = GetRemoteErrorData(structured); Assert.NotNull(data); - var errorData = data!.Value; + var errorData = data.Value; Assert.Equal("canvas_action_no_handler", errorData.GetProperty("code").GetString()); Assert.Equal("No handler implemented for this canvas action", errorData.GetProperty("message").GetString()); From 3eda9560cd21ac7e120c54d1e876b5b34cc6f46f Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 26 May 2026 11:48:59 -0400 Subject: [PATCH 6/6] Remove JsonRpcTests changes from PR Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/test/Unit/JsonRpcTests.cs | 68 -------------------------------- 1 file changed, 68 deletions(-) diff --git a/dotnet/test/Unit/JsonRpcTests.cs b/dotnet/test/Unit/JsonRpcTests.cs index a2cc1159c..6c045c8eb 100644 --- a/dotnet/test/Unit/JsonRpcTests.cs +++ b/dotnet/test/Unit/JsonRpcTests.cs @@ -78,50 +78,6 @@ static ValueTask HandleSingleObject(SingleObjectRequest re ValueTask.FromResult(new SingleObjectResponse { Value = request.Value }); } - [Fact] - public async Task JsonRpc_Preserves_Remote_Error_Data() - { - using var pair = JsonRpcReflectionPair.Create(); - - pair.Server.SetLocalRpcMethod( - "structuredError", - (Func>)((_, _, _) => throw CreateLocalRpcInvocationException( - -32603, - "No handler implemented for this canvas action", - CreateJsonElement("""{"code":"canvas_action_no_handler","message":"No handler implemented for this canvas action"}""")))); - pair.Server.SetLocalRpcMethod( - "nullErrorData", - (Func>)((_, _, _) => throw CreateLocalRpcInvocationException( - -32603, - "Null error data", - CreateJsonElement("null")))); - pair.Server.SetLocalRpcMethod( - "omittedErrorData", - (Func>)((_, _, _) => throw CreateLocalRpcInvocationException( - -32603, - "Omitted error data"))); - - pair.StartListening(); - - var structured = await Assert.ThrowsAnyAsync(() => - pair.Client.InvokeAsync("structuredError", ["invoke", 1])); - Assert.Equal(-32603, GetRemoteErrorCode(structured)); - - var data = GetRemoteErrorData(structured); - Assert.NotNull(data); - var errorData = data.Value; - Assert.Equal("canvas_action_no_handler", errorData.GetProperty("code").GetString()); - Assert.Equal("No handler implemented for this canvas action", errorData.GetProperty("message").GetString()); - - var nullData = await Assert.ThrowsAnyAsync(() => - pair.Client.InvokeAsync("nullErrorData", ["invoke", 1])); - Assert.Equal(JsonValueKind.Null, GetRemoteErrorData(nullData)?.ValueKind); - - var omittedData = await Assert.ThrowsAnyAsync(() => - pair.Client.InvokeAsync("omittedErrorData", ["invoke", 1])); - Assert.Null(GetRemoteErrorData(omittedData)); - } - [Fact] public async Task JsonRpc_Cancels_And_Disposes_Pending_Requests() { @@ -144,30 +100,6 @@ private static int GetRemoteErrorCode(Exception exception) return (int)property.GetValue(exception)!; } - private static JsonElement? GetRemoteErrorData(Exception exception) - { - var property = exception.GetType().GetProperty("ErrorData", BindingFlags.Instance | BindingFlags.Public); - Assert.NotNull(property); - return property.GetValue(exception) is JsonElement value ? value : null; - } - - private static JsonElement CreateJsonElement(string json) - { - using var document = JsonDocument.Parse(json); - return document.RootElement.Clone(); - } - - private static Exception CreateLocalRpcInvocationException(int code, string message, JsonElement? data = null) - { - var type = typeof(CopilotClient).Assembly.GetType("GitHub.Copilot.LocalRpcInvocationException", throwOnError: true)!; - return (Exception)Activator.CreateInstance( - type, - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, - binder: null, - args: [code, message, data], - culture: null)!; - } - private sealed class NamedParams { public string Name { get; set; } = string.Empty;