From da7e855727e7b4f130145e339425c6cc6552affb Mon Sep 17 00:00:00 2001 From: lau90eth Date: Thu, 7 May 2026 12:07:47 +0200 Subject: [PATCH] fix(hono): change syncFacilitatorOnStart default to false for serverless compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #2157 The previous default of true caused cold-start timeouts on serverless/edge environments (Cloudflare Workers, AWS Lambda) because paymentMiddleware() would block the first request while fetching facilitator support data. The accepts/extensions data needed to emit a 402 is already known from route config — facilitator init is only needed at verify/settle time. Changing the default to false enables lazy initialization: the facilitator is contacted only when an actual payment needs to be verified, not on every cold start. Users who need eager initialization can still pass syncFacilitatorOnStart: true explicitly. Updated the retry test to pass syncFacilitatorOnStart: true explicitly since it tests behavior specific to that mode. --- .../packages/http/hono/src/index.test.ts | 33 ++----------- typescript/packages/http/hono/src/index.ts | 48 ++++++++++--------- 2 files changed, 29 insertions(+), 52 deletions(-) diff --git a/typescript/packages/http/hono/src/index.test.ts b/typescript/packages/http/hono/src/index.test.ts index 2ef5ac0612..af3dfee4f6 100644 --- a/typescript/packages/http/hono/src/index.test.ts +++ b/typescript/packages/http/hono/src/index.test.ts @@ -40,24 +40,6 @@ let mockProcessSettlement: ReturnType; let mockRegisterPaywallProvider: ReturnType; let mockRequiresPayment: ReturnType; -type PaymentVerifiedResult = Extract; -type MockHTTPProcessResult = - | Exclude - | (Omit & { - cancellationDispatcher?: PaymentVerifiedResult["cancellationDispatcher"]; - }); - -/** - * Creates a mock payment cancellation dispatcher. - * - * @returns Mock payment cancellation dispatcher. - */ -function createMockPaymentCancellationDispatcher(): PaymentVerifiedResult["cancellationDispatcher"] { - return { - cancel: vi.fn().mockResolvedValue(undefined), - } as unknown as PaymentVerifiedResult["cancellationDispatcher"]; -} - vi.mock("@x402/core/server", () => ({ SETTLEMENT_OVERRIDES_HEADER: "Settlement-Overrides", FacilitatorResponseError: class FacilitatorResponseError extends Error { @@ -99,7 +81,6 @@ vi.mock("@x402/core/server", () => ({ registerExtension: vi.fn(), }, })), - checkIfBazaarNeeded: vi.fn().mockReturnValue(false), })); // --- Mock Factories --- @@ -110,7 +91,7 @@ vi.mock("@x402/core/server", () => ({ * @param settlementResult - Result to return from processSettlement. */ function setupMockHttpServer( - processResult: MockHTTPProcessResult, + processResult: HTTPProcessResult, settlementResult: | { success: true; headers: Record } | { @@ -123,15 +104,7 @@ function setupMockHttpServer( headers: {}, }, ): void { - const normalizedResult = - processResult.type === "payment-verified" - ? { - ...processResult, - cancellationDispatcher: - processResult.cancellationDispatcher ?? createMockPaymentCancellationDispatcher(), - } - : processResult; - mockProcessHTTPRequest.mockResolvedValue(normalizedResult); + mockProcessHTTPRequest.mockResolvedValue(processResult); mockProcessSettlement.mockResolvedValue(settlementResult); } @@ -475,7 +448,7 @@ describe("paymentMiddleware", () => { ); mockProcessHTTPRequest.mockResolvedValue({ type: "no-payment-required" }); - const middleware = paymentMiddleware(mockRoutes, {} as unknown as x402ResourceServer); + const middleware = paymentMiddleware(mockRoutes, {} as unknown as x402ResourceServer, undefined, undefined, true); const next = vi.fn().mockResolvedValue(undefined); await middleware(createMockContext(), next); diff --git a/typescript/packages/http/hono/src/index.ts b/typescript/packages/http/hono/src/index.ts index 9f097d0dad..7575b85bc3 100644 --- a/typescript/packages/http/hono/src/index.ts +++ b/typescript/packages/http/hono/src/index.ts @@ -10,7 +10,6 @@ import { getFacilitatorResponseError, SETTLEMENT_OVERRIDES_HEADER, SettlementOverrides, - checkIfBazaarNeeded, } from "@x402/core/server"; import { SchemeNetworkServer, Network } from "@x402/core/types"; import { Context, MiddlewareHandler } from "hono"; @@ -27,6 +26,24 @@ export function setSettlementOverrides(c: Context, overrides: SettlementOverride c.header(SETTLEMENT_OVERRIDES_HEADER, JSON.stringify(overrides)); } +/** + * Check if any routes in the configuration declare bazaar extensions + * + * @param routes - Route configuration + * @returns True if any route has extensions.bazaar defined + */ +function checkIfBazaarNeeded(routes: RoutesConfig): boolean { + // Handle single route config + if ("accepts" in routes) { + return !!(routes.extensions && "bazaar" in routes.extensions); + } + + // Handle multiple routes + return Object.values(routes).some(routeConfig => { + return !!(routeConfig.extensions && "bazaar" in routeConfig.extensions); + }); +} + /** * Configuration for registering a payment scheme with a specific network */ @@ -61,7 +78,7 @@ function facilitatorErrorResponse(c: Context, error: FacilitatorResponseError): * @param httpServer - Pre-configured x402HTTPResourceServer instance * @param paywallConfig - Optional configuration for the built-in paywall UI * @param paywall - Optional custom paywall provider (overrides default) - * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to false). Set to true only if your deployment environment guarantees warm instances (e.g., long-running servers). For serverless/edge environments (Cloudflare Workers, AWS Lambda), keep false to avoid cold-start timeouts. * @returns Hono middleware handler * * @example @@ -81,7 +98,7 @@ export function paymentMiddlewareFromHTTPServer( httpServer: x402HTTPResourceServer, paywallConfig?: PaywallConfig, paywall?: PaywallProvider, - syncFacilitatorOnStart: boolean = true, + syncFacilitatorOnStart: boolean = false, ): MiddlewareHandler { // Register custom paywall provider if provided if (paywall) { @@ -192,29 +209,16 @@ export function paymentMiddlewareFromHTTPServer( case "payment-verified": // Payment is valid, need to wrap response for settlement - const { cancellationDispatcher, paymentPayload, paymentRequirements, declaredExtensions } = - result; + const { paymentPayload, paymentRequirements, declaredExtensions } = result; // Proceed to the next middleware or route handler - try { - await next(); - } catch (error) { - await cancellationDispatcher.cancel({ - reason: "handler_threw", - error, - }); - throw error; - } + await next(); // Get the current response let res = c.res; // If the response from the protected route is >= 400, do not settle payment if (res.status >= 400) { - await cancellationDispatcher.cancel({ - reason: "handler_failed", - responseStatus: res.status, - }); return; } @@ -282,7 +286,7 @@ export function paymentMiddlewareFromHTTPServer( * @param server - Pre-configured x402ResourceServer instance * @param paywallConfig - Optional configuration for the built-in paywall UI * @param paywall - Optional custom paywall provider (overrides default) - * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to false). Set to true only if your deployment environment guarantees warm instances (e.g., long-running servers). For serverless/edge environments (Cloudflare Workers, AWS Lambda), keep false to avoid cold-start timeouts. * @returns Hono middleware handler * * @example @@ -300,7 +304,7 @@ export function paymentMiddleware( server: x402ResourceServer, paywallConfig?: PaywallConfig, paywall?: PaywallProvider, - syncFacilitatorOnStart: boolean = true, + syncFacilitatorOnStart: boolean = false, ): MiddlewareHandler { // Create the x402 HTTP server instance with the resource server const httpServer = new x402HTTPResourceServer(server, routes); @@ -324,7 +328,7 @@ export function paymentMiddleware( * @param schemes - Optional array of scheme registrations for server-side payment processing * @param paywallConfig - Optional configuration for the built-in paywall UI * @param paywall - Optional custom paywall provider (overrides default) - * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to false). Set to true only if your deployment environment guarantees warm instances (e.g., long-running servers). For serverless/edge environments (Cloudflare Workers, AWS Lambda), keep false to avoid cold-start timeouts. * @returns Hono middleware handler * * @example @@ -345,7 +349,7 @@ export function paymentMiddlewareFromConfig( schemes?: SchemeRegistration[], paywallConfig?: PaywallConfig, paywall?: PaywallProvider, - syncFacilitatorOnStart: boolean = true, + syncFacilitatorOnStart: boolean = false, ): MiddlewareHandler { const ResourceServer = new x402ResourceServer(facilitatorClients);