Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions typescript/packages/core/src/facilitator/x402Facilitator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,11 @@ export class x402Facilitator {
};

for (const hook of this.afterVerifyHooks) {
await hook(resultContext);
try {
await hook(resultContext);
} catch (hookError) {
console.error("afterVerify hook threw an error (payment succeeded):", hookError);
}
}

return verifyResult;
Expand Down Expand Up @@ -464,7 +468,11 @@ export class x402Facilitator {
};

for (const hook of this.afterSettleHooks) {
await hook(resultContext);
try {
await hook(resultContext);
} catch (hookError) {
console.error("afterSettle hook threw an error (settlement succeeded):", hookError);
}
}

return settleResult;
Expand Down
12 changes: 10 additions & 2 deletions typescript/packages/core/src/server/x402ResourceServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,11 @@ export class x402ResourceServer {
};

for (const hook of this.afterVerifyHooks) {
await hook(resultContext);
try {
await hook(resultContext);
} catch (hookError) {
console.error("afterVerify hook threw an error (payment succeeded):", hookError);
}
}

return verifyResult;
Expand Down Expand Up @@ -875,7 +879,11 @@ export class x402ResourceServer {
};

for (const hook of this.afterSettleHooks) {
await hook(resultContext);
try {
await hook(resultContext);
} catch (hookError) {
console.error("afterSettle hook threw an error (settlement succeeded):", hookError);
}
}

// Let declared extensions add data to settlement response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,4 +331,51 @@ describe("x402Facilitator - Lifecycle Hooks", () => {
expect(result).toBe(facilitator);
});
});

describe("hook error isolation", () => {
it("should not treat payment as failed when afterVerify hook throws", async () => {
const facilitator = new x402Facilitator();
facilitator.register("eip155:8453", new MockSchemeFacilitator());

facilitator.onAfterVerify(async () => {
throw new Error("Hook side-effect failed");
});

// Payment verification succeeded — hook error must not surface as a verify failure
const result = await facilitator.verify(buildPaymentPayload(), buildPaymentRequirements());
expect(result.isValid).toBe(true);
});

it("should not treat settlement as failed when afterSettle hook throws", async () => {
const facilitator = new x402Facilitator();
facilitator.register("eip155:8453", new MockSchemeFacilitator());

facilitator.onAfterSettle(async () => {
throw new Error("Hook side-effect failed");
});

// Settlement succeeded — hook error must not surface as a settle failure
const result = await facilitator.settle(buildPaymentPayload(), buildPaymentRequirements());
expect(result.success).toBe(true);
});

it("should continue remaining afterVerify hooks after one throws", async () => {
const facilitator = new x402Facilitator();
facilitator.register("eip155:8453", new MockSchemeFacilitator());

let secondHookCalled = false;

facilitator
.onAfterVerify(async () => {
throw new Error("First hook fails");
})
.onAfterVerify(async () => {
secondHookCalled = true;
});

await facilitator.verify(buildPaymentPayload(), buildPaymentRequirements());
expect(secondHookCalled).toBe(true);
});
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,43 @@ describe("x402ResourceServer", () => {
});
});

describe("hook error isolation", () => {
it("should not treat payment as failed when afterVerify hook throws", async () => {
server.onAfterVerify(async () => {
throw new Error("Hook side-effect failed");
});

// Hook error must not surface as a verify failure
const result = await server.verifyPayment(buildPaymentPayload(), buildPaymentRequirements());
expect(result.isValid).toBe(true);
});

it("should continue remaining afterVerify hooks after one throws", async () => {
let secondHookCalled = false;

server
.onAfterVerify(async () => {
throw new Error("First hook fails");
})
.onAfterVerify(async () => {
secondHookCalled = true;
});

await server.verifyPayment(buildPaymentPayload(), buildPaymentRequirements());
expect(secondHookCalled).toBe(true);
});

it("should not treat settlement as failed when afterSettle hook throws", async () => {
server.onAfterSettle(async () => {
throw new Error("Hook side-effect failed");
});

// Hook error must not surface as a settle failure
const result = await server.settlePayment(buildPaymentPayload(), buildPaymentRequirements());
expect(result.success).toBe(true);
});
});

describe("onVerifyFailure", () => {
it("should execute when verification fails", async () => {
let hookExecuted = false;
Expand Down
Loading