X Tutup
Skip to content

feat(Client): Introduce client side interceptors#796

Open
guglielmo-san wants to merge 13 commits intoa2aproject:1.0-devfrom
guglielmo-san:guglielmoc/rework_client_interceptors
Open

feat(Client): Introduce client side interceptors#796
guglielmo-san wants to merge 13 commits intoa2aproject:1.0-devfrom
guglielmo-san:guglielmoc/rework_client_interceptors

Conversation

@guglielmo-san
Copy link
Member

Description

This PR refactors the client interceptors architecture, centralizing their execution within the BaseClient rather than delegating them to the underlying transport implementations. It also introduces strict generic type definitions to strongly type the inputs and results for interceptor hooks across all supported client methods.

@guglielmo-san guglielmo-san changed the title Guglielmoc/rework client interceptors feat(Client): Introduce client side interceptors Mar 10, 2026
@guglielmo-san
Copy link
Member Author

/gemini review

@guglielmo-san guglielmo-san marked this pull request as ready for review March 10, 2026 10:21
@guglielmo-san guglielmo-san requested a review from a team as a code owner March 10, 2026 10:21
@guglielmo-san guglielmo-san requested a review from ishymko March 10, 2026 10:21
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, 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 refactors the client-side interceptor architecture. The primary goal is to centralize the application of interceptors within the BaseClient, moving away from transport-specific implementations. This change ensures a unified and predictable interception flow across all client operations. Additionally, the new design incorporates robust generic typing for interceptor hooks, enhancing code clarity and maintainability. The refactoring also involved reorganizing related classes and renaming concepts for better consistency.

Highlights

  • Centralized Interceptor Execution: Interceptor logic has been moved from individual transport implementations to the BaseClient, ensuring all client methods consistently apply interceptors.
  • Strongly Typed Interceptor Hooks: Introduced strict generic type definitions for before and after interceptor hooks, improving type safety and developer experience for all client methods.
  • Refactored Interceptor Structure: The middleware.py module has been removed, and its core components, ClientCallContext and ClientCallInterceptor, have been relocated and redefined in client.py and a new interceptors.py module, respectively. The term 'middleware' has been consistently replaced with 'interceptors'.
  • Interceptor Lifecycle Management: New internal methods (_execute_with_interceptors, _intercept_before, _intercept_after) were added to BaseClient to manage the interceptor lifecycle, including support for early returns.
Changelog
  • src/a2a/client/init.py
    • Updated imports for ClientCallContext and ClientCallInterceptor to reflect their new locations
    • Added CallInterceptor to the module's __all__ export list
  • src/a2a/client/auth/credentials.py
    • Updated the import path for ClientCallContext
  • src/a2a/client/auth/interceptor.py
    • Updated the import path for ClientCallContext
    • Removed ClientCallInterceptor from the base classes of AuthInterceptor
  • src/a2a/client/base_client.py
    • Added new imports for interceptor-related types (AfterArgs, BeforeArgs, ClientCallInput, ClientCallResult, M, P, R, UnionAfterArgs, UnionBeforeArgs)
    • Renamed the middleware parameter to interceptors in the __init__ method
    • Introduced a new _interceptors attribute to store the list of interceptors
    • Modified all client methods (send_message, get_task, list_tasks, cancel_task, create_task_push_notification_config, get_task_push_notification_config, list_task_push_notification_configs, delete_task_push_notification_config, subscribe, get_extended_agent_card) to use the new _execute_with_interceptors method
    • Updated _process_stream to incorporate before_args and apply _intercept_after to stream responses
    • Added new private asynchronous methods: _execute_with_interceptors for orchestrating interceptor calls and transport execution, _intercept_before for processing pre-call interceptors, and _intercept_after for processing post-call interceptors
  • src/a2a/client/client.py
    • Added imports for BaseModel, Field from pydantic, and MutableMapping
    • Moved the ClientCallContext class definition from middleware.py to this file
    • Updated the import path for ClientCallInterceptor
    • Renamed the middleware parameter to interceptors in the Client.__init__ method
    • Renamed the _middleware attribute to _interceptors
    • Renamed the add_request_middleware method to add_interceptor
  • src/a2a/client/client_factory.py
    • Updated the import path for ClientCallInterceptor
    • Removed the interceptors parameter from the create calls for JsonRpcTransport, RestTransport, and GrpcTransport within _register_defaults and grpc_transport_producer
  • src/a2a/client/interceptors.py
    • Added a new file defining the core interceptor types and abstract base class
    • Defined ClientCallInput to represent method input payload
    • Defined ClientCallResult to represent method result payload
    • Defined BeforeArgs for arguments passed to interceptors before a method call, including support for early returns
    • Defined AfterArgs for arguments passed to interceptors after a method call completes
    • Introduced ClientCallInterceptor as an abstract base class with before and after abstract methods
    • Created UnionBeforeArgs and UnionAfterArgs TypeAliases for strongly typing interceptor arguments across various client methods
  • src/a2a/client/middleware.py
    • Removed the file, as its contents were refactored and moved to client.py and interceptors.py
  • src/a2a/client/transports/base.py
    • Updated the import path for ClientCallContext
  • src/a2a/client/transports/grpc.py
    • Updated the import path for ClientCallContext
    • Removed the import for ClientCallInterceptor
    • Removed the interceptors parameter from the create method
  • src/a2a/client/transports/http_helpers.py
    • Updated the import path for ClientCallContext
  • src/a2a/client/transports/jsonrpc.py
    • Updated the import path for ClientCallContext
    • Removed the import for ClientCallInterceptor
    • Removed the interceptors parameter from the __init__ method
  • src/a2a/client/transports/rest.py
    • Updated the import path for ClientCallContext
    • Removed the import for ClientCallInterceptor
    • Removed the interceptors parameter from the __init__ method
  • src/a2a/client/transports/tenant_decorator.py
    • Updated the import path for ClientCallContext
  • src/a2a/compat/v0_3/grpc_transport.py
    • Updated the import path for ClientCallContext
    • Removed the import for ClientCallInterceptor
    • Removed the interceptors parameter from the create method
  • tests/client/test_base_client.py
    • Renamed the middleware parameter to interceptors in the base_client fixture
  • tests/client/test_base_client_interceptors.py
    • Added a new test file to verify the functionality of the centralized interceptor logic in BaseClient
    • Included tests for normal interceptor flow and early return scenarios
  • tests/client/test_client_factory_grpc.py
    • Removed the interceptors parameter from mock_grpc.create calls in various test cases
  • tests/client/transports/test_grpc_client.py
    • Updated the import path for ClientCallContext in test functions
  • tests/client/transports/test_jsonrpc_client.py
    • Removed the test_init_with_interceptors test case
    • Updated the import path for ClientCallContext in test functions
  • tests/client/transports/test_rest_client.py
    • Updated the import path for ClientCallContext in test functions
  • tests/integration/test_client_server_integration.py
    • Renamed the middleware parameter to interceptors in BaseClient initialization within integration tests
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.

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 significantly refactors the client-side interceptor architecture, centralizing logic in BaseClient and introducing strongly-typed generic interfaces for hooks, which improves maintainability and type safety. However, the refactor is incomplete as it breaks the existing AuthInterceptor by not updating it to the new interface, rendering authentication non-functional. Additionally, a type mismatch in handling early returns for streaming methods (send_message and subscribe) could cause runtime errors. There are also a few minor areas for code simplification and a small bug in the public API exports.



class AuthInterceptor(ClientCallInterceptor):
class AuthInterceptor:
Copy link
Contributor

Choose a reason for hiding this comment

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

security-high high

The AuthInterceptor class has been modified, removing its inheritance from ClientCallInterceptor (line 11) and not updating it to the new before and after methods required by the refactored interceptor architecture in BaseClient. This oversight breaks the primary authentication mechanism for the client SDK, as any attempt to use it with the new BaseClient will result in an AttributeError. To resolve this, AuthInterceptor must be updated to implement the new ClientCallInterceptor interface from a2a.client.interceptors. This involves implementing async def before(self, args: UnionBeforeArgs) -> None: to adapt the existing authentication logic to modify args.context.service_parameters and a pass-through async def after(self, args: UnionAfterArgs) -> None: method.

from a2a.client.interceptors import ClientCallInterceptor, UnionBeforeArgs, UnionAfterArgs

class AuthInterceptor(ClientCallInterceptor):
    # ... __init__ is the same ...

    async def before(self, args: UnionBeforeArgs) -> None:
        agent_card = args.agent_card
        context = args.context
        # ... existing logic to get credentials ...

        # Create context and service_parameters if they don't exist
        if context is None:
            context = ClientCallContext()
            args.context = context
        if context.service_parameters is None:
            context.service_parameters = {}

        # Add headers to service_parameters
        # e.g., context.service_parameters['Authorization'] = f'Bearer {credential}'
        # ... rest of the logic ...

    async def after(self, args: UnionAfterArgs) -> None:
        pass

)
yield after_args.result.value
return

Copy link
Contributor

Choose a reason for hiding this comment

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

security-medium medium

In the send_message method, when an interceptor triggers an early return for a streaming request, the code yields after_args.result.value (line 137). However, for streaming requests, the interceptor's expected result type is StreamResponse, while the send_message method is expected to yield ClientEvent (a tuple of StreamResponse and Task). Yielding a single StreamResponse instead of a tuple will cause a TypeError or unexpected behavior in the calling code that consumes the iterator, potentially leading to a denial of service for applications using the SDK.

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 refactors the client-side interceptor (formerly middleware) system. It introduces a new interceptors.py module defining a more granular ClientCallInterceptor interface with before and after methods, replacing the single intercept method. The ClientCallContext class is moved to client.py. The BaseClient is updated to centralize interceptor execution logic, including handling early returns for both streaming and non-streaming calls, and all transport classes are modified to no longer directly manage interceptors. Review comments highlight a typo in the __all__ export list, suggest refactoring duplicated interceptor logic in streaming methods, and point out redundant checks in interceptor handling.

Comment on lines +110 to +136
before_args: BeforeArgs[
Literal['send_message_streaming'],
SendMessageRequest,
StreamResponse,
] = BeforeArgs(
input=ClientCallInput(
method='send_message_streaming', value=request
),
agent_card=self._card,
context=context,
)
before_result = await self._intercept_before(before_args)

if before_result is not None:
after_args = AfterArgs(
result=ClientCallResult(
method=before_args.input.method,
value=before_result['early_return'].value,
),
agent_card=self._card,
context=before_args.context,
)
await self._intercept_after(
cast('UnionAfterArgs', after_args), before_result['executed']
)
yield after_args.result.value
return
Copy link
Contributor

Choose a reason for hiding this comment

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

high

This block of code for handling before interceptors and early returns is nearly identical to the block in the subscribe method (lines 390-412). This duplication makes the code harder to maintain and prone to inconsistencies if one block is updated but the other is not.

To improve maintainability, consider refactoring this logic into a private helper method or an async context manager. This would centralize the streaming interception logic, similar to how _execute_with_interceptors centralizes it for non-streaming calls.

…plify default list assignments in the `Client` constructor.
…xecute_stream_with_interceptors` and add `_format_stream_event`.
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.

1 participant

X Tutup