From f5d79f465e35e2920603ba57424e064f6a1caab9 Mon Sep 17 00:00:00 2001 From: Ayush Ojha Date: Wed, 25 Mar 2026 00:43:50 -0700 Subject: [PATCH 1/2] Fix HTTPFacilitatorClient not following 308 redirects from facilitator The x402.org/facilitator/supported endpoint returns HTTP 308 before resolving to 200. HTTPFacilitatorClient did not normalize the base URL or explicitly request redirect following, causing syncFacilitatorOnStart to silently fail in some runtimes. When no supported payment kinds are loaded, the middleware passes all requests through as 200 instead of 402. - Strip trailing slashes from facilitator URL in constructor to prevent unnecessary 308 redirects from trailing-slash normalization - Explicitly set redirect: "follow" on all fetch calls (verify, settle, getSupported) for cross-runtime compatibility - Add tests for URL normalization and redirect option propagation Closes #1692 --- .../.changeset/fix-facilitator-redirect.md | 5 + .../core/src/http/httpFacilitatorClient.ts | 7 +- .../unit/http/httpFacilitatorClient.test.ts | 104 ++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 typescript/.changeset/fix-facilitator-redirect.md diff --git a/typescript/.changeset/fix-facilitator-redirect.md b/typescript/.changeset/fix-facilitator-redirect.md new file mode 100644 index 0000000000..39b9902c64 --- /dev/null +++ b/typescript/.changeset/fix-facilitator-redirect.md @@ -0,0 +1,5 @@ +--- +'@x402/core': patch +--- + +Fixed HTTPFacilitatorClient not following 308 redirects from facilitator endpoints. Normalized base URL to strip trailing slashes and explicitly set `redirect: "follow"` on all fetch calls for cross-runtime compatibility. diff --git a/typescript/packages/core/src/http/httpFacilitatorClient.ts b/typescript/packages/core/src/http/httpFacilitatorClient.ts index 1cde198336..37bd2a0674 100644 --- a/typescript/packages/core/src/http/httpFacilitatorClient.ts +++ b/typescript/packages/core/src/http/httpFacilitatorClient.ts @@ -191,7 +191,9 @@ export class HTTPFacilitatorClient implements FacilitatorClient { * @param config - Configuration options for the facilitator client */ constructor(config?: FacilitatorConfig) { - this.url = config?.url || DEFAULT_FACILITATOR_URL; + // Normalize URL: strip trailing slashes to prevent redirect loops (e.g. 308) + // when constructing endpoint paths like `${url}/supported` + this.url = (config?.url || DEFAULT_FACILITATOR_URL).replace(/\/+$/, ""); this._createAuthHeaders = config?.createAuthHeaders; } @@ -218,6 +220,7 @@ export class HTTPFacilitatorClient implements FacilitatorClient { const response = await fetch(`${this.url}/verify`, { method: "POST", headers, + redirect: "follow", body: JSON.stringify({ x402Version: paymentPayload.x402Version, paymentPayload: this.toJsonSafe(paymentPayload), @@ -269,6 +272,7 @@ export class HTTPFacilitatorClient implements FacilitatorClient { const response = await fetch(`${this.url}/settle`, { method: "POST", headers, + redirect: "follow", body: JSON.stringify({ x402Version: paymentPayload.x402Version, paymentPayload: this.toJsonSafe(paymentPayload), @@ -318,6 +322,7 @@ export class HTTPFacilitatorClient implements FacilitatorClient { const response = await fetch(`${this.url}/supported`, { method: "GET", headers, + redirect: "follow", }); if (response.ok) { diff --git a/typescript/packages/core/test/unit/http/httpFacilitatorClient.test.ts b/typescript/packages/core/test/unit/http/httpFacilitatorClient.test.ts index e1721158e3..03cc844138 100644 --- a/typescript/packages/core/test/unit/http/httpFacilitatorClient.test.ts +++ b/typescript/packages/core/test/unit/http/httpFacilitatorClient.test.ts @@ -163,4 +163,108 @@ describe("HTTPFacilitatorClient", () => { expect(result.errorMessage).toBeUndefined(); expect(result.payer).toBeUndefined(); }); + + describe("URL normalization", () => { + it("strips trailing slashes from the configured URL", () => { + const client = new HTTPFacilitatorClient({ url: "https://x402.org/facilitator/" }); + expect(client.url).toBe("https://x402.org/facilitator"); + }); + + it("strips multiple trailing slashes", () => { + const client = new HTTPFacilitatorClient({ url: "https://x402.org/facilitator///" }); + expect(client.url).toBe("https://x402.org/facilitator"); + }); + + it("leaves URLs without trailing slash unchanged", () => { + const client = new HTTPFacilitatorClient({ url: "https://x402.org/facilitator" }); + expect(client.url).toBe("https://x402.org/facilitator"); + }); + + it("uses default URL when no config is provided", () => { + const client = new HTTPFacilitatorClient(); + expect(client.url).toBe("https://x402.org/facilitator"); + }); + }); + + describe("redirect handling", () => { + it("passes redirect: follow to fetch on getSupported", async () => { + const mockFetch = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" }], + }), + { status: 200 }, + ), + ); + vi.stubGlobal("fetch", mockFetch); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + await client.getSupported(); + + expect(mockFetch).toHaveBeenCalledWith( + "https://facilitator.test/supported", + expect.objectContaining({ redirect: "follow" }), + ); + }); + + it("passes redirect: follow to fetch on verify", async () => { + const mockFetch = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ isValid: true }), + { status: 200 }, + ), + ); + vi.stubGlobal("fetch", mockFetch); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + await client.verify(paymentPayload, paymentRequirements); + + expect(mockFetch).toHaveBeenCalledWith( + "https://facilitator.test/verify", + expect.objectContaining({ redirect: "follow" }), + ); + }); + + it("passes redirect: follow to fetch on settle", async () => { + const mockFetch = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + success: true, + transaction: "0xabc", + network: "eip155:8453", + }), + { status: 200 }, + ), + ); + vi.stubGlobal("fetch", mockFetch); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + await client.settle(paymentPayload, paymentRequirements); + + expect(mockFetch).toHaveBeenCalledWith( + "https://facilitator.test/settle", + expect.objectContaining({ redirect: "follow" }), + ); + }); + + it("constructs correct endpoint URLs after trailing slash normalization", async () => { + const mockFetch = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" }], + }), + { status: 200 }, + ), + ); + vi.stubGlobal("fetch", mockFetch); + + const client = new HTTPFacilitatorClient({ url: "https://x402.org/facilitator/" }); + await client.getSupported(); + + expect(mockFetch).toHaveBeenCalledWith( + "https://x402.org/facilitator/supported", + expect.anything(), + ); + }); + }); }); From 6064faf5055e999247b97083a8c06e550833d124 Mon Sep 17 00:00:00 2001 From: Ayush Ojha Date: Mon, 30 Mar 2026 00:50:27 -0700 Subject: [PATCH 2/2] fix: apply prettier formatting to httpFacilitatorClient test --- .../core/test/unit/http/httpFacilitatorClient.test.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/typescript/packages/core/test/unit/http/httpFacilitatorClient.test.ts b/typescript/packages/core/test/unit/http/httpFacilitatorClient.test.ts index 03cc844138..ef2214edbf 100644 --- a/typescript/packages/core/test/unit/http/httpFacilitatorClient.test.ts +++ b/typescript/packages/core/test/unit/http/httpFacilitatorClient.test.ts @@ -208,12 +208,9 @@ describe("HTTPFacilitatorClient", () => { }); it("passes redirect: follow to fetch on verify", async () => { - const mockFetch = vi.fn().mockResolvedValue( - new Response( - JSON.stringify({ isValid: true }), - { status: 200 }, - ), - ); + const mockFetch = vi + .fn() + .mockResolvedValue(new Response(JSON.stringify({ isValid: true }), { status: 200 })); vi.stubGlobal("fetch", mockFetch); const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" });