@@ -1076,16 +1076,16 @@ async def handle(self, request: CheckoutRequest) -> CheckoutResult:
10761076 else self .compose_mppx is not None
10771077 )
10781078 if enforced and malformed is not None :
1079- # Protocol-correct recovery: a junk credential gets a FRESH 402
1080- # challenge (same shape as the discovery leg) so an x402/MPP
1081- # client re-pays, instead of a dead-end 400 it cannot act on.
1082- # Strip the malformed credential first (``_discovery_view``) so
1083- # recipient minting + MPP compose take their fresh-mint /
1084- # fresh-challenge path rather than binding the garbage and
1085- # raising another 400. pre_validate and the gate/assess are
1086- # skipped by construction here (a junk credential must never burn
1087- # the merchant's paid probe or an identity API call) .
1088- result = await self ._emit_fresh_challenge (self ._discovery_view ( ctx ))
1079+ # A junk credential is treated as a discovery request: strip it
1080+ # and re-enter handle() so pre_validate + pricing + recipient
1081+ # minting + compose all run their fresh path exactly as for a
1082+ # no-credential request. That yields a fresh 402 the agent
1083+ # re-pays against, not a dead-end 400, and not a 500 when
1084+ # compute_pricing reads state that pre_validate populates. The
1085+ # gate/assess and settle are skipped by construction: after
1086+ # stripping there is no payment header, so no re-trigger of this
1087+ # check (max one level of recursion) and no identity call.
1088+ result = await self .handle (self ._strip_payment_headers ( request ))
10891089 return dataclasses .replace (result , settle_phase = "credential_malformed" )
10901090
10911091 # Pre-validate (optional): resolve merchant-specific per-request state
@@ -2658,11 +2658,11 @@ async def _handle_mppx(self, ctx: CheckoutContext) -> CheckoutResult:
26582658 )
26592659
26602660 async def _emit_fresh_challenge (self , ctx : CheckoutContext ) -> CheckoutResult :
2661- """Emit a fresh 402 challenge ( the discovery leg) .
2661+ """Emit the discovery- leg 402 .
26622662
2663- Factored out so the malformed-credential path can reuse it. Idempotent on
2664- already-computed pricing / resolved recipients (``_emit_402`` resolves
2665- recipients), so the normal discovery leg pays nothing extra .
2663+ pre_validate + pricing already ran in the main flow before this is
2664+ reached; idempotent on already-computed pricing / resolved recipients
2665+ (``_emit_402`` resolves recipients), so it primes nothing twice .
26662666 """
26672667 if ctx .pricing is None :
26682668 ctx .pricing = await _maybe_await (self .compute_pricing (ctx ))
@@ -2679,24 +2679,26 @@ async def _emit_fresh_challenge(self, ctx: CheckoutContext) -> CheckoutResult:
26792679 pass
26802680 return await self ._emit_402 (ctx , mppx_headers = mppx_headers )
26812681
2682- def _discovery_view (self , ctx : CheckoutContext ) -> CheckoutContext :
2683- """Return a copy of ``ctx`` with payment-credential headers removed.
2682+ def _strip_payment_headers (self , request : CheckoutRequest ) -> CheckoutRequest :
2683+ """Return a copy of the request with payment-credential headers removed.
26842684
2685- Recipient minting and MPP compose then take their discovery (fresh-mint,
2686- fresh-challenge) path instead of binding the inbound credential, turning a
2687- malformed-credential request into a clean 402 re-challenge. Pricing and
2688- recipients are reset so they mint fresh.
2685+ Re-entering handle() with it treats the request as a discovery
2686+ (no-credential) request: pre_validate + pricing + minting + compose run
2687+ their fresh path, and the gate/assess and settle are skipped. Turns a
2688+ malformed-credential request into a clean 402 re-challenge. The raw
2689+ request is left intact; compose_mppx reads it only best-effort under a
2690+ try/except, while the stripped headers are what the shape check, gate
2691+ dispatch, and recipient minting read.
26892692 """
26902693 headers = {
26912694 k : v
2692- for k , v in ctx . request .headers .items ()
2695+ for k , v in request .headers .items ()
26932696 if not (
26942697 k .lower () in ("payment-signature" , "x-payment" )
26952698 or (k .lower () == "authorization" and v .startswith ("Payment " ))
26962699 )
26972700 }
2698- request = dataclasses .replace (ctx .request , headers = headers )
2699- return dataclasses .replace (ctx , request = request , pricing = None , recipients = {})
2701+ return dataclasses .replace (request , headers = headers )
27002702
27012703 async def _emit_402 (
27022704 self ,
0 commit comments