refactor: centralize server validation by kabir · Pull Request #1213 · a2aproject/a2a-java · GitHub
Skip to content

refactor: centralize server validation - #1213

Open
kabir wants to merge 2 commits into
a2aproject:mainfrom
kabir:valiation-unification
Open

kabir wants to merge 2 commits into
a2aproject:mainfrom
kabir:valiation-unification

Conversation

@kabir

@kabir kabir commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

This fixes #1093

BREAKING CHANGE
It changes how integrations interact with the transport/* handlers.
The public user API remains the same

@kabir
kabir force-pushed the valiation-unification branch 5 times, most recently from bb224c9 to 6d43ff6 Compare October 20, 2026 12:57
@kabir
kabir requested a review from ehsavoie October 20, 2026 19:34

@ehsavoie ehsavoie left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Medium Severity

  1. AgentCardOperation.java:12-16 — Latent NPE from nullable capabilityFailure
    capabilityFailure is @nullable but AgentCardRequestPolicy.validateCapability calls Objects.requireNonNull() on it. Any caller constructing new AgentCardOperation(predicate, null) will NPE at runtime. Only safe today because AgentCardOperation.none() pairs null with an always-true predicate. Add constructor validation or a typed factory.

  2. DefaultRequestHandler.java:1273 — isBlank() vs isNullOrEmpty() inconsistency
    validateRequiredId uses id.isBlank() while authorizeTaskAccess uses Assert.isNullOrEmpty() (which accepts whitespace). A whitespace-only ID passes one guard but fails the other — inconsistent behavior within the same class. Unify on one check.

  3. RestHandler_v0_3.java (8+ sites) — Redundant isNullOrEmpty + checkNotNullParam pattern
    Since isNullOrEmpty returning false already guarantees non-null, the follow-up checkNotNullParam call is only there for NullAway. Extract a requireNonEmpty helper or use @SuppressWarnings.

  4. JSONRPCHandler.java:729 — Dead TenantNotFoundException catch block
    preflight() at line 745-751 already catches TenantNotFoundException and wraps it as InvalidParamsError. resolveExtendedDiscoveryCard returns null, never throws it. Branch is unreachable and misleading.

Low Severity

  1. RestHandler_v0_3.java:228-237 — Negative historyLength returns 500 instead of 422
    Integer.parseInt("-1") succeeds with no bounds check — negative historyLength escapes format parsing and causes a 500 InternalError downstream instead of a 422 InvalidParamsError. Add historyLength < 0 → InvalidParamsError_v0_3.

  2. GrpcHandler.java:743,750 — Per-request allocation of policy objects
    AgentCardResolver and AgentCardRequestPolicy are allocated on every request via createAgentCardResolver(). JSONRPCHandler and RestHandler create them once in @PostConstruct. Cache in a field or use lazy init.

  3. AgentCardResolver.java:31-32 — Tenant normalized twice
    AgentCardRequestPolicy.normalizeTenant() normalizes before calling resolveEffectiveCard(), which internally calls normalizeTenantForResolution() again. Idempotent today, maintenance trap tomorrow.

  4. JSONRPCRequestDecoder_v0_3.java:109-111 — Null body relies on undocumented Gson behavior
    Null body rejection relies on JsonParser.parseString(null) throwing rather than an explicit null check. Fragile across Gson version upgrades.

  5. A2AServerRoutesTest.java — Missing blank line before @test
    Missing blank line before testEmptyMessageIdDecodeErrorRetainsId — formatting inconsistency with the rest of the file.


kabir added 2 commits October 21, 2026 16:52
Unify tenant/card resolution, transport validation, and effective-card context across REST, JSON-RPC, and gRPC while preserving transport-specific error ordering.

Add the targeted compat-0.3 REST push-config canonicalization and JSON-RPC ID correctness fixes.

This fixes a2aproject#1093
@kabir
kabir force-pushed the valiation-unification branch from 23dd672 to 072e123 Compare October 21, 2026 14:55
@kabir
kabir requested a review from ehsavoie October 21, 2026 15:20
@kabir
kabir dismissed ehsavoie’s stale review October 21, 2026 15:20

I believe I dealt with everything

@ehsavoie ehsavoie left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Breaking API change: Confirmed. RestHandler.getTask, listTasks, and listTaskPushNotificationConfigurations all changed from typed params (Integer, Boolean, int) to raw String. These are public methods, so any external subclass or caller breaks silently. The commit is labeled feat: instead of feat!: — that's a real oversight.

taskId validation repeated 7× in RestHandler and GrpcHandler canonicalTenant repeated ~10×

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is dead code

* @param validatedCards set tracking which cards have already been validated
* @return the resolved agent card
*/
public static AgentCard resolveAndValidateOnce(Instance<AgentCard> agentCardInstance,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is dead code

* @param validator validation logic to apply on first access
* @return the resolved agent card
*/
public static AgentCard resolveAndValidateOnce(Supplier<AgentCard> agentCardSupplier,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is dead code

* @return the resolved agent card
* @throws IllegalStateException if neither card is available
*/
public static AgentCard resolveWithFallback(Instance<AgentCard> publicCard,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is dead code

*
* @see #resolveWithFallback(Instance, Instance, AgentCardRouter, String, Set, Consumer)
*/
public static AgentCard resolveWithFallback(Instance<AgentCard> publicCard,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is dead code


private static final Logger LOGGER = Logger.getLogger(JSONRPCHandler.class.getName());

private static final AgentCardOperation STREAMING = new AgentCardOperation(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Duplicated in RestHandler and GrpcHandler

private static final AgentCardOperation STREAMING = new AgentCardOperation(
card -> card.capabilities().streaming(),
() -> new UnsupportedOperationError(null, "Streaming is not supported by the agent", null));
private static final AgentCardOperation PUSH_NOTIFICATIONS = new AgentCardOperation(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Duplicated in RestHandler and GrpcHandler with PUSH

* @return {@code true} if the value is {@code null} or empty
*/
public static boolean isNullOrEmpty(@Nullable String value) {
return value == null || value.isEmpty();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A taskId of " " passes all validation gates and hits the backend as a nonsense ID
return value == null || value.isBlank();

} catch (JsonSyntaxException e) {
return new DecodeFailure(requestId, methodName, new JSONParseError_v0_3(e.getMessage()));
} catch (Throwable t) {
return new DecodeFailure(requestId, methodName, new InternalError_v0_3(t.getMessage()));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should n't this be sanitized ?


// Validate method field
com.google.gson.JsonElement methodElement = node.get("method");
if (methodElement == null || !methodElement.isJsonPrimitive()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

isJsonPrimitive() returns true for any JSON primitive — strings, booleans, and numbers alike. So "method": true passes the guard, getAsString() coerces it to the string "true", and the switch falls to default → MethodNotFoundError_v0_3 (-32601) instead of InvalidRequestError_v0_3 (-3260)

It should be:

if (methodElement == null || !methodElement.isJsonPrimitive()
        || !methodElement.getAsJsonPrimitive().isString()) {
    throw new InvalidRequestError_v0_3("...");
}

* <li>{@link IllegalStateException} if nothing resolves</li>
* </ol>
*/
private static AgentCard resolveDefaultWithRouterFallback(Instance<AgentCard> publicCard,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Dead code

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Dead code

This branch has not been deployed

No deployments
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.

Push validation into DefaultRequestHandler

2 participants