Skip to content

Commit 9b29ed6

Browse files
MertBasar0claude
andcommitted
Fix DeleteTaskPushNotificationConfigAsync throwing on valid null result
DeleteTaskPushNotificationConfig is the only A2A method with a void result. The generic JSON-RPC send helper unconditionally deserialized the response result and threw A2AException when it was null, so the client rejected the spec-compliant null-result success response that A2AJsonRpcProcessor itself produces. Split the transport/error handling into SendJsonRpcRequestCoreAsync and route delete through a void-result overload that skips result deserialization. Fixes #429. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 8fe65cf commit 9b29ed6

2 files changed

Lines changed: 31 additions & 3 deletions

File tree

src/A2A/Client/A2AClient.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public async Task<ListTaskPushNotificationConfigResponse> ListTaskPushNotificati
9191
/// <inheritdoc />
9292
public async Task DeleteTaskPushNotificationConfigAsync(DeleteTaskPushNotificationConfigRequest request, CancellationToken cancellationToken = default)
9393
{
94-
await SendJsonRpcRequestAsync<object>(A2AMethods.DeleteTaskPushNotificationConfig, request, cancellationToken).ConfigureAwait(false);
94+
await SendJsonRpcRequestAsync(A2AMethods.DeleteTaskPushNotificationConfig, request, cancellationToken).ConfigureAwait(false);
9595
}
9696

9797
/// <inheritdoc />
@@ -111,6 +111,25 @@ public void Dispose()
111111
[UnconditionalSuppressMessage("AOT", "IL2026:RequiresUnreferencedCode", Justification = "All types are registered in source-generated JsonContext.")]
112112
[UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "All types are registered in source-generated JsonContext.")]
113113
private async Task<TResult> SendJsonRpcRequestAsync<TResult>(string method, object? @params, CancellationToken cancellationToken)
114+
{
115+
var rpcResponse = await SendJsonRpcRequestCoreAsync(method, @params, cancellationToken).ConfigureAwait(false);
116+
117+
return rpcResponse.Result.Deserialize<TResult>(A2AJsonUtilities.DefaultOptions)
118+
?? throw new A2AException("Failed to deserialize JSON-RPC result.", A2AErrorCode.InternalError);
119+
}
120+
121+
/// <summary>Sends a JSON-RPC request for methods whose successful response carries no result, e.g. a null <c>result</c> member.</summary>
122+
/// <param name="method">The JSON-RPC method name.</param>
123+
/// <param name="params">The request parameters to serialize, if any.</param>
124+
/// <param name="cancellationToken">A token to cancel the operation.</param>
125+
private async Task SendJsonRpcRequestAsync(string method, object? @params, CancellationToken cancellationToken)
126+
{
127+
_ = await SendJsonRpcRequestCoreAsync(method, @params, cancellationToken).ConfigureAwait(false);
128+
}
129+
130+
[UnconditionalSuppressMessage("AOT", "IL2026:RequiresUnreferencedCode", Justification = "All types are registered in source-generated JsonContext.")]
131+
[UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "All types are registered in source-generated JsonContext.")]
132+
private async Task<JsonRpcResponse> SendJsonRpcRequestCoreAsync(string method, object? @params, CancellationToken cancellationToken)
114133
{
115134
using var activity = A2ADiagnostics.Source.StartActivity($"A2AClient/{method}", ActivityKind.Client);
116135
var stopwatch = Stopwatch.StartNew();
@@ -146,8 +165,7 @@ private async Task<TResult> SendJsonRpcRequestAsync<TResult>(string method, obje
146165
throw new A2AException(error.Message, (A2AErrorCode)error.Code);
147166
}
148167

149-
return rpcResponse.Result.Deserialize<TResult>(A2AJsonUtilities.DefaultOptions)
150-
?? throw new A2AException("Failed to deserialize JSON-RPC result.", A2AErrorCode.InternalError);
168+
return rpcResponse;
151169
}
152170
catch (Exception ex)
153171
{

tests/A2A.UnitTests/Client/A2AClientTests.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,16 @@ public async Task DeletePushNotificationConfigAsync_SendsCorrectMethod()
372372
Assert.Equal(A2AMethods.DeleteTaskPushNotificationConfig, requestJson.RootElement.GetProperty("method").GetString());
373373
}
374374

375+
[Fact]
376+
public async Task DeletePushNotificationConfigAsync_CompletesOnNullResult()
377+
{
378+
// A2AJsonRpcProcessor answers delete with a success response whose result is null,
379+
// which is valid per JSON-RPC 2.0 for the only void-result A2A method.
380+
var sut = CreateA2AClient(new JsonRpcResponse { Id = "test-id", Result = null });
381+
382+
await sut.DeleteTaskPushNotificationConfigAsync(new DeleteTaskPushNotificationConfigRequest { Id = "cfg-1", TaskId = "t-1" });
383+
}
384+
375385
private static A2AClient CreateA2AClient(object result, Action<HttpRequestMessage>? onRequest = null, bool isSse = false)
376386
{
377387
var response = new JsonRpcResponse

0 commit comments

Comments
 (0)