Skip to content

fix(payments): address langgraph middleware review follow-ups#570

Merged
aidandaly24 merged 2 commits into
mainfrom
fix/payments-langgraph-review-followups
Jul 8, 2026
Merged

fix(payments): address langgraph middleware review follow-ups#570
aidandaly24 merged 2 commits into
mainfrom
fix/payments-langgraph-review-followups

Conversation

@aidandaly24

Copy link
Copy Markdown
Contributor

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

  1. Async on_payment_error callback on the sync path now fails loudly.
    The guard that detects an async callback used with sync .invoke() previously raised a TypeError inside the callback try, whose except Exception immediately caught it, logged it, and returned None (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.

  2. Async callable objects (async def __call__) are now detected.
    inspect.iscoroutinefunction returns False for 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.

  3. Post-payment retry-rejection honors custom handlers.
    _check_retry_rejection hardcoded GenericPaymentHandler + 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.

  4. Payment header injection preserved when a tool call omits args.
    _detect_402 used request.tool_call.get("args", {}), which returns a throwaway dict when there is no args key, so header injection mutated a dict the retried handler never read. Switched to setdefault("args", {}) so the header reaches the retried call.

Docs

  1. Documented the custom-handler input contract.
    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 a custom_handlers value 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 legitimate PaymentResponseHandler/GenericPaymentHandler subclasses (the existing TrackingHandler functional 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_header and _inject_for_error_retry, the redundant name-based detection fallback for generic tools, and _FallbackHandler not subclassing PaymentResponseHandler.

Testing

  • Added regression tests for all four correctness fixes; each was verified to fail against the merged middleware.py and 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).
  • Full LangGraph unit suite green: 146 passed (up from 141).
  • Full payments suite green: 648 passed.
  • ruff check and ruff format --check clean 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.

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.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

✅ No Breaking Changes Detected

No public API breaking changes found in this PR.

@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label Jul 7, 2026
@agentcore-devx-automation

Copy link
Copy Markdown
Contributor

Claude Security Review: no high-confidence findings. (run)

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label Jul 7, 2026
Comment thread src/bedrock_agentcore/payments/integrations/langgraph/middleware.py Outdated
Comment thread src/bedrock_agentcore/payments/integrations/langgraph/middleware.py

@jariy17 jariy17 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.
@agentcore-devx-automation

Copy link
Copy Markdown
Contributor

Claude Security Review: no high-confidence findings. (run)

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label Jul 7, 2026
@aidandaly24 aidandaly24 enabled auto-merge (squash) July 7, 2026 21:18
@aidandaly24 aidandaly24 merged commit 46a0bea into main Jul 8, 2026
43 checks passed
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