Skip to content

Conversation

@sangmeshcp
Copy link

No description provided.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @sangmeshcp, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the flexibility of client-side interceptors by allowing them to not only observe but also actively modify or replace the data flowing through the client. This change empowers developers to implement more advanced use cases such as data transformation, encryption, or dynamic mocking directly within the interceptor chain, without altering the core client logic.

Highlights

  • Interceptor Payload Modification: Client interceptors can now modify or completely replace both the request and response payloads before and after the actual API call, respectively.
  • Updated Interceptor Signatures: The internal interceptBefore and interceptAfter methods have been updated to return the modified payloads, which are then used in the subsequent call or as the final result.
  • Comprehensive Client Method Updates: All client methods that utilize interceptors have been refactored to correctly handle the new return values from the interceptor functions, including necessary type assertions for the modified payloads.
  • New End-to-End Test: An end-to-end test has been added to validate the functionality of interceptors replacing request and response payloads, ensuring the feature works as expected.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@sangmeshcp sangmeshcp changed the title Allow replacing request and response in client interceptors fix: Allow replacing request and response in client interceptors Jan 16, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a powerful feature allowing client interceptors to replace request and response payloads. The implementation, however, has a critical flaw across all modified methods in a2aclient/client.go: it uses direct, unsafe type assertions on the payloads returned from interceptors. This can lead to panics if an interceptor returns a payload of an incorrect type or a nil payload where one is expected.

I've added two detailed comments with suggestions for GetTask (as an example of a simple RPC) and SendStreamingMessage (as an example of a streaming RPC) to demonstrate how to fix this using safe type assertions. This pattern of checking types should be applied consistently to all other methods modified in this PR to ensure robustness.

Comment on lines +74 to +82
query = iReq.(*a2a.TaskQueryParams)

resp, err := c.transport.GetTask(ctx, query)
if errOverride := c.interceptAfter(ctx, method, resp, err); errOverride != nil {
return nil, errOverride
iResp, finalErr := c.interceptAfter(ctx, method, resp, err)
if finalErr != nil {
return nil, finalErr
}

return resp, err
return iResp.(*a2a.Task), nil
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The direct type assertions for both the request and response payloads in this function are unsafe and can cause a panic. An interceptor could return a payload of an incorrect type, or a nil response, which would crash the application.

You should use the value, ok form of type assertion to validate the types and handle nil values gracefully before using them. This same issue exists in CancelTask, SendMessage, and the various ...TaskPushConfig methods.

Here is a suggested implementation for GetTask that addresses this. Note that you will need to import the fmt package. A similar approach should be taken for the other non-streaming methods.

    newQuery, ok := iReq.(*a2a.TaskQueryParams)
	if !ok {
		return nil, fmt.Errorf("interceptor returned unexpected request type: got %T, want *a2a.TaskQueryParams", iReq)
	}
	query = newQuery

	resp, err := c.transport.GetTask(ctx, query)
	iResp, finalErr := c.interceptAfter(ctx, method, resp, err)
	if finalErr != nil {
		return nil, finalErr
	}

	task, ok := iResp.(*a2a.Task)
	if !ok {
		return nil, fmt.Errorf("interceptor returned unexpected response type: got %T, want *a2a.Task", iResp)
	}
	return task, nil

Comment on lines +134 to 164
message = iReq.(*a2a.MessageSendParams)

if card := c.card.Load(); card != nil && !card.Capabilities.Streaming {
resp, err := c.transport.SendMessage(ctx, message)
if errOverride := c.interceptAfter(ctx, method, resp, err); errOverride != nil {
yield(nil, errOverride)
iResp, finalErr := c.interceptAfter(ctx, method, resp, err)
if finalErr != nil {
yield(nil, finalErr)
return
}
yield(resp, err)
yield(iResp.(a2a.Event), nil)
return
}

for resp, err := range c.transport.SendStreamingMessage(ctx, message) {
if errOverride := c.interceptAfter(ctx, method, resp, err); errOverride != nil {
yield(nil, errOverride)
iResp, finalErr := c.interceptAfter(ctx, method, resp, err)
if finalErr != nil {
yield(nil, finalErr)
return
}
// When err is nil, finalErr is nil (unless interceptor added error)
// But if err was NOT nil, transport loop yields error.
// interceptAfter will see err, and return finalErr = err (or modified).
// So if finalErr != nil, we yield it and return.

if err != nil {
yield(nil, err)
return
}
// If transport sends (nil, err), interceptAfter sees (nil, err).
// If it returns (nil, finalErr), we yield (nil, finalErr).

if !yield(resp, nil) {
if !yield(iResp.(a2a.Event), nil) {
return
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

As with the non-streaming methods, the type assertions in this streaming function are unsafe. This applies to the request payload and the response payloads in both the non-streaming fallback and the streaming loop. An incorrect type from an interceptor will cause a panic.

Please add safe type checks for all assertions. Here is a suggested implementation for this function. A similar approach should be used for ResubscribeToTask. Note that you will need to import the fmt package.

		newMessage, ok := iReq.(*a2a.MessageSendParams)
		if !ok {
			yield(nil, fmt.Errorf("interceptor returned unexpected request type: got %T, want *a2a.MessageSendParams", iReq))
			return
		}
		message = newMessage

		if card := c.card.Load(); card != nil && !card.Capabilities.Streaming {
			resp, err := c.transport.SendMessage(ctx, message)
			iResp, finalErr := c.interceptAfter(ctx, method, resp, err)
			if finalErr != nil {
				yield(nil, finalErr)
				return
			}
			event, ok := iResp.(a2a.Event)
			if !ok {
				yield(nil, fmt.Errorf("interceptor returned unexpected response type: got %T, want a2a.Event", iResp))
				return
			}
			yield(event, nil)
			return
		}

		for resp, err := range c.transport.SendStreamingMessage(ctx, message) {
			iResp, finalErr := c.interceptAfter(ctx, method, resp, err)
			if finalErr != nil {
				yield(nil, finalErr)
				return
			}
			// When err is nil, finalErr is nil (unless interceptor added error)
			// But if err was NOT nil, transport loop yields error.
			// interceptAfter will see err, and return finalErr = err (or modified).
			// So if finalErr != nil, we yield it and return.

			// If transport sends (nil, err), interceptAfter sees (nil, err).
			// If it returns (nil, finalErr), we yield (nil, finalErr).
			event, ok := iResp.(a2a.Event)
			if !ok {
				yield(nil, fmt.Errorf("interceptor returned unexpected response type: got %T, want a2a.Event", iResp))
				return
			}
			if !yield(event, nil) {
				return
			}
		}

if err != nil {
return nil, err
}
query = iReq.(*a2a.TaskQueryParams)
Copy link
Member

Choose a reason for hiding this comment

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

I'd rather make interceptBefore and interceptAfter into free generic functions and do type assertions there, for example:

Details
diff --git a/a2aclient/client.go b/a2aclient/client.go
index a796319..c59986b 100644
--- a/a2aclient/client.go
+++ b/a2aclient/client.go
@@ -16,6 +16,7 @@ package a2aclient

 import (
 	"context"
+	"fmt"
 	"iter"
 	"sync/atomic"

@@ -67,11 +68,10 @@ func (c *Client) AddCallInterceptor(ci CallInterceptor) {
 func (c *Client) GetTask(ctx context.Context, query *a2a.TaskQueryParams) (*a2a.Task, error) {
 	method := "GetTask"

-	ctx, iReq, err := c.interceptBefore(ctx, method, query)
+	ctx, query, err := interceptBefore(ctx, c, method, query)
 	if err != nil {
 		return nil, err
 	}
-	query = iReq.(*a2a.TaskQueryParams)

 	resp, err := c.transport.GetTask(ctx, query)
 	iResp, finalErr := c.interceptAfter(ctx, method, resp, err)
@@ -316,7 +316,7 @@ func (c *Client) withDefaultSendConfig(message *a2a.MessageSendParams, blocking
 	return &result
 }

-func (c *Client) interceptBefore(ctx context.Context, method string, payload any) (context.Context, any, error) {
+func interceptBefore[T any](ctx context.Context, c *Client, method string, payload *T) (context.Context, *T, error) {
 	req := Request{
 		Method:  method,
 		BaseURL: c.baseURL,
@@ -337,7 +337,12 @@ func (c *Client) interceptBefore(ctx context.Context, method string, payload any
 		ctx = localCtx
 	}

-	return withCallMeta(ctx, req.Meta), req.Payload, nil
+	newPayload, ok := req.Payload.(*T)
+	if !ok {
+		return ctx, nil, fmt.Errorf("request type changed from %T to %T", payload, req.Payload)
+	}
+
+	return withCallMeta(ctx, req.Meta), newPayload, nil
 }

 func (c *Client) interceptAfter(ctx context.Context, method string, payload any, err error) (any, error) {

@yarolegovich
Copy link
Member

closing because #177 and #181 was implemented

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.

2 participants