Pass the response message to the aio gRPC client response hook - #4938
Open
alliasgher wants to merge 2 commits into
Open
Pass the response message to the aio gRPC client response hook#4938alliasgher wants to merge 2 commits into
alliasgher wants to merge 2 commits into
Conversation
add_done_callback cannot take a coroutine, so _wrap_unary_response pre-fetched code and details and handed details -- the gRPC status detail string -- to the response hook. Callers got '' instead of the response. The sync client (_client.py) and the aio streaming path both pass the deserialized message, so this was internally inconsistent. Await the call to get the response before registering the callback. That is free: await call.code() already blocks until the RPC has completed, and grpc.aio caches the unary result, so the caller's own await still returns it. The extra await is skipped when no hook is registered. The hook is now called only on OK. Awaiting a failed call would raise AioRpcError inside the try block, which would re-raise from the interceptor rather than the caller and skip add_done_callback entirely, leaking the span. This matches the sync client, which never reaches its hook on error.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3490
Description
The aio client's unary
response_hookreceives the gRPC status detail string instead of the response message, so it is''on a successful call.add_done_callbackcannot take a coroutine, so_wrap_unary_responsepre-fetchescodeanddetailsand passes them into the callback factory. The callback then doesresponse_hook(span, details)—detailswas never the response, it just happened to be in scope.This is an internal inconsistency rather than a design question. Both of the other paths already pass the deserialized message:
_client.py:112-113(sync):if self._response_hook: self._call_response_hook(span, response)_aio_client.py_wrap_stream_response:self._call_response_hook(span, response)Only the aio unary path differs. It affects unary-unary and stream-unary.
Fix
await callto get the response before registering the callback. That is free latency-wise —await call.code()above it already blocks until the RPC has completed — andgrpc.aiocaches the unary result, so the caller's ownawaitstill returns the response. The extra await is skipped entirely when no hook is registered.Two judgement calls I want to flag rather than have you find
The hook no longer fires on error. Today it fires with
''on a non-OK status; now it fires only on OK. That matches the sync client, which never reaches its hook when the call raises, but it is a behaviour change for anyone relying on the empty-string call.I deliberately did not
await callon the error path. Doing so raisesAioRpcErrorinside thetry, which theexcept grpc.aio.AioRpcErrorcatches and re-raises from the interceptor rather than from the caller's ownawait— andadd_done_callbackis then never registered, sospan.end()never runs and the span leaks.The hooks have no docstrings specifying their arguments, so the case for "response message" rests on parity with
_client.pyand_wrap_stream_responserather than on documented contract. Happy to be told the aio unary hook was meant to be different.Tests
The existing test asserted the bug:
Passing a protobuf message to
set_attributeis silently dropped, so it could not simply be left alone. The hook now mirrors the sync test'sresponse.response_data, and on currentmainthat fails withAttributeError: 'str' object has no attribute 'response_data'.Added two cases: stream-unary (same bug, separate call path) and one asserting the hook does not fire on a failed RPC.
pytest tests/in the grpc package: 142 passed. There are 10 pre-existing failures inTestOpenTelemetryServerInterceptorUnixin my environment (Unix domain sockets on macOS) — I diffed the failing-test sets with and without this change via junit XML and they are identical, so nothing here is a regression. Ruff check and format are clean.I ran against grpcio 1.75.1 (the pinned test requirement). I did not separately run against the oldest supported grpcio in
test-requirements-0.txt;add_done_callbackand awaiting a completed unary call are long-stable, but flagging that I did not verify it.