Skip to content

fix(seller): retry x402 middleware setup instead of caching a failed or empty build - #44

Open
chopmob-cloud wants to merge 1 commit into
aws-samples:mainfrom
chopmob-cloud:fix/seller-x402-middleware-cache-retry
Open

fix(seller): retry x402 middleware setup instead of caching a failed or empty build#44
chopmob-cloud wants to merge 1 commit into
aws-samples:mainfrom
chopmob-cloud:fix/seller-x402-middleware-cache-retry

Conversation

@chopmob-cloud

Copy link
Copy Markdown

Summary

In backend/lambdas/sellers/image-gen/index.js, getPaymentMiddleware() memoizes the middleware build promise unconditionally:

if (!_middlewarePromise) {
  _middlewarePromise = (async () => { ... return null | middleware ... })();
}
return _middlewarePromise;

Two of the promise's outcomes are not safe to cache for the life of a warm Lambda container:

  1. Rejection. httpServer.initialize() fetches /supported from the x402 facilitator. If that call fails (a transient facilitator outage, a cold-start network blip), the promise rejects and the rejected promise is cached. Every later request in that container then awaits the same rejected promise and returns 500, until an unrelated cold start, even after the facilitator recovers.
  2. null (no wallet yet). If the first request arrives before Seller Setup provisions the payout wallet, buildAccepts returns [], the build resolves to null, and that null is cached. The route keeps returning the 503 "Seller not configured" response for the life of the container even after Setup runs.

Both are self-inflicted stranding: the container gets stuck on the first outcome it happened to see.

Fix

Cache only a successfully built middleware. Keep sharing the in-flight promise so concurrent first requests do not each rebuild, but clear the slot when the build resolves to null or rejects, so the next request retries.

async function getPaymentMiddleware() {
  if (!_middlewarePromise) _middlewarePromise = buildPaymentMiddleware();
  try {
    const middleware = await _middlewarePromise;
    if (!middleware) _middlewarePromise = null; // retry once the wallet is provisioned
    return middleware;
  } catch (err) {
    _middlewarePromise = null; // retry after a transient build/facilitator failure
    throw err;
  }
}

A successfully built middleware is still cached for the life of the container, so the steady-state behavior is unchanged.

Validation

Standalone reproduction of the caching wrappers (old vs new) against a build stub that fails once (facilitator down), then returns null (no wallet), then succeeds:

OLD -> req1..3 = [throw(500), throw(500), throw(500)] | req4 = throw(500) | total builds = 1
NEW -> req1..3 = [throw(500), null(503), REAL]        | req4 = REAL       | total builds = 3

OLD strands on its first outcome forever (one build, never retries). NEW retries after the transient failure and after Setup, then caches the successful middleware (build count stops at 3, so a successful build is still memoized and not rebuilt on later requests). node --check passes on the changed file.

Happy to add a unit test for getPaymentMiddleware if you would like one in the suite.

…or empty build

getPaymentMiddleware memoized the build promise unconditionally, so a null
result (seller wallet not provisioned yet) or a rejection (e.g. a transient
x402 facilitator outage during httpServer.initialize()) was cached for the
life of the warm Lambda container. Every later request then replayed that
first outcome, returning 503 or 500 until an unrelated cold start, even after
Seller Setup ran or the facilitator recovered.

Cache only a successfully built middleware: share the in-flight promise so
concurrent first requests do not each rebuild, but clear the slot on a null
or rejected result so the next request retries.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant