fix(payments): address langgraph middleware review follow-ups#570
Merged
Conversation
Follow-ups to the LangGraph payments middleware merged in #546: - Async on_payment_error callback on the sync .invoke() path now raises a clear TypeError instead of being caught by the callback's own except block and silently degrading to PROPAGATE. Detection also covers callable objects with an async __call__, which the async path now awaits (previously leaked an un-awaited coroutine). - The post-payment retry-rejection check routes through the registered custom handler (raw content) so a tool with a bespoke 402 format has its rejection detected and its error detail surfaced, instead of the still-402 response being returned to the LLM as a normal result. - Payment-header injection is preserved when a tool call arrives without an "args" key (setdefault instead of get), so the retried call actually sends the header. - Documented the custom-handler raw-content input contract in the config docstring and the middleware README. Adds regression tests that fail against the merged code and pass with these fixes.
Contributor
✅ No Breaking Changes DetectedNo public API breaking changes found in this PR. |
Contributor
|
Claude Security Review: no high-confidence findings. (run) |
jariy17
reviewed
Jul 7, 2026
jariy17
reviewed
Jul 7, 2026
jariy17
requested changes
Jul 7, 2026
jariy17
left a comment
Contributor
There was a problem hiding this comment.
Just need to catch all types of async calls.
Address review feedback on #570: - _is_async_callback now unwraps functools.partial before inspecting, so a partial wrapping an async function, an async callable object, or a nested partial is detected. inspect.iscoroutinefunction misses partial(async_callable_object) even on Python 3.10, and partial async-ness is not reliably visible across Python versions. Added regression tests for the sync (raise) and async (await) partial paths. - Trim the args setdefault comment to state only why, not how.
Contributor
|
Claude Security Review: no high-confidence findings. (run) |
jariy17
approved these changes
Jul 7, 2026
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.
Description of changes
Follow-up fixes to the LangGraph payments middleware merged in #546. These came out of a post-merge review of
middleware.py. All are scoped to the LangGraph integration; no public API changes.Correctness
Async
on_payment_errorcallback on the sync path now fails loudly.The guard that detects an async callback used with sync
.invoke()previously raised aTypeErrorinside the callbacktry, whoseexcept Exceptionimmediately caught it, logged it, and returnedNone(PROPAGATE). The developer's misconfiguration was swallowed into a log line and an error message indistinguishable from a real payment failure. The check now runs before the try and propagates, matching the "fail loudly" intent stated in the original commit and class docstring.Async callable objects (
async def __call__) are now detected.inspect.iscoroutinefunctionreturnsFalsefor a callable instance whose__call__is async, so such a callback slipped past the guard on the sync path (leaked an un-awaited coroutine, then degraded to PROPAGATE) and was never awaited on the async path. Detection now inspects the bound__call__; the sync path raises and the async path awaits it.Post-payment retry-rejection honors custom handlers.
_check_retry_rejectionhardcodedGenericPaymentHandler+ JSON fallback, so when a tool with a registered custom handler returned a bespoke-format 402 after payment (server rejected the signed payment), the still-402 went undetected and was handed back to the LLM as a normal result. It now routes through the custom handler (raw content), mirroring_detect_402, and surfaces the real error detail.Payment header injection preserved when a tool call omits
args._detect_402usedrequest.tool_call.get("args", {}), which returns a throwaway dict when there is noargskey, so header injection mutated a dict the retried handler never read. Switched tosetdefault("args", {})so the header reaches the retried call.Docs
Custom handlers receive the raw
ToolMessage.content(str or list of blocks), not the middleware's internal wrapped shape. The built-in handlers expect the normalized shape, so passing one of them directly as acustom_handlersvalue silently fails to detect 402s. Clarified this in the config docstring and README. This is docs-only on purpose: a type-based config guard would wrongly reject legitimatePaymentResponseHandler/GenericPaymentHandlersubclasses (the existingTrackingHandlerfunctional test is exactly that).Behavior change to flag for reviewers
Fix #1 changes async-callback-on-sync from "log + PROPAGATE" to "raise
TypeError", and updates the two tests that asserted the swallowed behavior. If the graceful fallthrough was intentional, the alternative is to keep it and only correct the misleading "fail loudly" wording instead. Happy to switch.Deferred (noted, not addressed here)
Non-correctness cleanups left for a separate change: the
"PROPAGATE"magic-string sentinel in_handle_callback_resolution, the remaining near-duplication between_inject_payment_headerand_inject_for_error_retry, the redundant name-based detection fallback for generic tools, and_FallbackHandlernot subclassingPaymentResponseHandler.Testing
middleware.pyand pass with these changes (temporarily swapped the merged file back in and ran the new tests: 6 failed / 1 guard-passed, then all pass once restored).146 passed(up from 141).648 passed.ruff checkandruff format --checkclean on all changed files.By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.