Skip to content

fix(core): fail closed when a gate hook throws - #3702

Open
Bartok9 wants to merge 2 commits into
x402-foundation:mainfrom
Bartok9:bartok9/gate-hook-fail-closed
Open

Bartok9 wants to merge 2 commits into
x402-foundation:mainfrom
Bartok9:bartok9/gate-hook-fail-closed

Conversation

@Bartok9

@Bartok9 Bartok9 commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #3689.

beforeVerify and beforeSettle are payment gates. A hook can return { abort: true } to stop the payment, but a throw was only logged and the loop continued. An extension that could not finish its check (decode, signature, commitment) therefore failed open.

A throw in those two phases now stops the payment with reason extension_hook_error and the thrown message. The warning log stays. Advisory phases (after*, on*Failure, enrichment) still log and continue.

Test plan

  • pnpm exec vitest run test/unit/server/x402ResourceServer.test.ts --coverage.enabled=false in typescript/packages/core — 162 passed
  • throwing beforeVerify returns isValid: false and does not call the facilitator
  • throwing beforeSettle throws SettleError with errorReason: extension_hook_error and does not settle
  • explicit abort results are unchanged

Majority-AI disclosure: drafted and reviewed with AI assistance.

The Vercel preview gate may ask the Coinbase Team to authorize a first contribution from this fork. That is not a code failure.

beforeVerify and beforeSettle are payment gates. A throw was logged and
treated as a pass, so an extension that could not finish its check still
authorized the payment. Those phases now abort with
extension_hook_error. Advisory hooks still log and continue.

Fixes x402-foundation#3689.

Majority-AI disclosure: drafted and reviewed with AI assistance; tests run locally.
@vercel

vercel Bot commented Oct 5, 2026

Copy link
Copy Markdown

@Bartok9 is attempting to deploy a commit to the Coinbase Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added typescript sdk Changes to core v2 packages labels Oct 5, 2026
@javierpmateos

Copy link
Copy Markdown
Contributor

Thanks for picking this up so quickly, @Bartok9. This is pretty much the direction I was hoping for in #3689, and @StratedgeWorkflowSystems's cross-SDK check makes the case clearly.

I checked it at 751590a, and Go and Python both stop when the hook throws (go/server.go L1217-1219 and L1497-1500, python/x402/server.py L234-239), and the TS facilitator does the same. So this brings the TS resource server in line with the other SDKs, rather than introducing TS-specific behaviour.

One small thing I'd consider: when the hook throws, the error message ends up in invalidMessage / errorMessage, which is then sent back to the paying client. With an explicit abort, the extension author controls that message, but with a throw it could contain something internal, like an RPC URL, file path, or storage error.

Maybe we should return a fixed message such as "extension hook failed" and keep the actual error details in the existing warning log. That would avoid leaking internal details while still giving the extension author enough information to debug it.

Also, one thing worth keeping in mind for the merge: #3151 currently wraps the TS facilitator's beforeVerify / beforeSettle loops in the same log-and-continue pattern. That would make the facilitator fail-open at the same time the server is moving to fail-closed.

A gate-hook throw still fails closed, but invalidMessage and errorMessage
now use a fixed "extension hook failed". The thrown detail stays in the
existing warning log so an RPC URL or path is not sent to the client.
@Bartok9

Bartok9 commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

Good catch on the client-facing text. Pushed bc6fca27: a throw in beforeVerify / beforeSettle still fails closed (extension_hook_error, facilitator not called), but invalidMessage / errorMessage is now the fixed string extension hook failed. The thrown message stays in the existing warning log, so an RPC URL, path, or storage error is not sent back to the payer. Explicit abort messages are unchanged — the extension author still controls those.

On #3151: agreed that wrapping the TS facilitator's gate loops in log-and-continue would reopen fail-open on that path while this PR closes it on the resource server. This change does not touch the facilitator. Happy to follow that up separately so the two do not land in opposite directions.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sdk Changes to core v2 packages typescript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: a throwing beforeVerify/beforeSettle hook is ignored, so extension gates fail open

2 participants