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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions