Skip to content

A2AClient.DeleteTaskPushNotificationConfigAsync always throws "Failed to deserialize JSON-RPC result" on a valid null result #429

Description

@gijswalraven

Summary

A2AClient.DeleteTaskPushNotificationConfigAsync always throws A2AException: "Failed to deserialize JSON-RPC result." against a spec-compliant server — including the SDK's own server. The delete succeeds on the server; the client cannot consume the (valid) empty result. DeleteTaskPushNotificationConfig is the only A2A method with a void result, so it is the only one affected — every other method returns a non-null result and round-trips correctly.

Environment

  • Packages: A2A, A2A.AspNetCore, A2A.V0_3Compat — all 1.0.0-preview2
  • Transport: JSON-RPC over HTTP (v1.0 / proto-style)
  • .NET 10

Root cause — client requires a non-null result

The generic send helper unconditionally deserializes result and throws when it is null:

// src/A2A/Client/A2AClient.cs:149-150
return rpcResponse.Result.Deserialize<TResult>(A2AJsonUtilities.DefaultOptions)
    ?? throw new A2AException("Failed to deserialize JSON-RPC result.", A2AErrorCode.InternalError);

Delete routes through that helper with TResult = object, so there is no path that tolerates a null result:

// src/A2A/Client/A2AClient.cs:92-95
public async Task DeleteTaskPushNotificationConfigAsync(DeleteTaskPushNotificationConfigRequest request, CancellationToken cancellationToken = default)
{
    await SendJsonRpcRequestAsync<object>(A2AMethods.DeleteTaskPushNotificationConfig, request, cancellationToken).ConfigureAwait(false);
}

The server produces exactly that null result

Both server-side processors return a successful JSON-RPC response whose result is null for delete:

// src/A2A.AspNetCore/A2AJsonRpcProcessor.cs:159-162
case A2AMethods.DeleteTaskPushNotificationConfig:
    var deletePnConfig = DeserializeAndValidate<DeleteTaskPushNotificationConfigRequest>(parameters.Value);
    await requestHandler.DeleteTaskPushNotificationConfigAsync(deletePnConfig, cancellationToken).ConfigureAwait(false);
    response = JsonRpcResponse.CreateJsonRpcResponse(requestId, (object?)null);
    break;

(src/A2A.V0_3Compat/V03ServerProcessor.cs does the same in its v1.0 dispatch.)

A null result on a successful response is valid per JSON-RPC 2.0 — result MUST be present on success, and null is a legal value. So the server is correct and the client rejects its own server's valid response.

Reproduction

  1. Host any server that implements DeleteTaskPushNotificationConfigAsync (the SDK's own A2AServer once a push-config store is wired in, or a custom IA2ARequestHandler).
  2. Create a task, register a push config, then call from the client:
await client.DeleteTaskPushNotificationConfigAsync(
    new DeleteTaskPushNotificationConfigRequest { TaskId = taskId, Id = configId });

Expected

Completes without throwing. The config is deleted (and it is, server-side).

Actual

Throws:

A2A.A2AException: Failed to deserialize JSON-RPC result.

Observed wire response (server → client)

{"jsonrpc":"2.0","id":"<id>"}

i.e. a success response with no/null result. The client then evaluates rpcResponse.Result.Deserialize<object>() ?? throw, which throws.

CreateTaskPushNotificationConfig, GetTaskPushNotificationConfig, and ListTaskPushNotificationConfig round-trip fine over the same client/server pair; only delete fails.

Suggested fix (client side)

Give void-result methods a path that does not demand a non-null result. Any of:

  1. Add a no-result overload, e.g. SendJsonRpcRequestAsync(string method, object? @params, CancellationToken) that sends the request, surfaces a JSON-RPC error if present, and returns without deserializing result. Have DeleteTaskPushNotificationConfigAsync call it.
  2. In the generic helper, when TResult permits null (or the JSON result is null/absent) and no error is present, return default instead of throwing.

Either keeps the spec-compliant null-result response from being treated as a failure. (Server side is already correct; no change needed there.)

Notes

  • Discovered while backing the push-config store methods (which 1.0.0-preview2 answers with -32003) via an IA2ARequestHandler decorator. With the store wired in, create/get/list work end-to-end; delete is the lone exception due to this client-side check.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions