Skip to main content
Version: next

Resilience audit rules

Audit rules to verify that a Front-Commerce project degrades gracefully when a backend or third-party service fails.

An e-commerce storefront depends on remote services — the e-commerce backend, a CMS, reviews, recommendations, payment providers. These rules verify that the failure of any one of them degrades the experience instead of taking the site down, and that failures stay visible to operators instead of being silently swallowed.

AUDIT-RES-01 — Never let a third-party service failure break page rendering

Severity: critical — Detection: static + runtime

Rule: No page render (SSR loader or client component) may depend unconditionally on a non-essential third-party service (reviews, recommendations, chat, A/B testing, marketing scripts): when the service is down or slow, the page must render without that block.

Why: A real audit found a home page completely unusable because api.guaranteed-reviews.com was down — the reviews widget's failure propagated to the whole route. An outage in a cosmetic service must never become a storefront outage.

How to check:

  1. Inventory outbound third-party calls:

    grep -rEn "https?://" app/ extensions/ --include="*.ts" --include="*.tsx" \
    | grep -v "front-commerce\|localhost\|example.com\|\.mdx"

    For each third-party call reached from a route loader or a GraphQL resolver on a key page, verify the call is wrapped in error handling that returns a fallback value (empty list, null) instead of throwing to the route.

  2. Check that the corresponding components tolerate the fallback: a component receiving null/empty data must render nothing or a placeholder, not crash.

  3. Runtime: simulate the outage. Point the third-party hostname to a black hole (for example add 127.0.0.2 api.thirdparty.com to /etc/hosts of the app server, or block it at the network level), restart, and load the home, category, and product pages. A violation is any page returning a 5xx or an error boundary instead of a degraded page.

AUDIT-RES-02 — Set timeouts and error handling on every outbound call

Severity: important — Detection: static

Rule: Every outbound HTTP call made from GraphQL loaders, resolvers, or route loaders must define an explicit timeout and handle its failure path (catch, log, and return a domain error or fallback).

Why: The default fetch has no timeout: a hanging upstream keeps requests (and their server resources) open until the client gives up. Under traffic, one slow third party exhausts the Node.js event loop and connection pool, turning a partial outage into a full one.

How to check:

  1. List outbound calls in server code:

    grep -rn "fetch(\|axios" extensions/ app/ --include="*.ts" | grep -v spec
  2. For each call site, check for a timeout mechanism: AbortSignal.timeout(...) passed as signal, an axios timeout option, or a client-level default configured where the HTTP client is created. A call with no timeout at any level is a violation.

  3. Check the failure path: the await must be inside a try/catch (or the promise must have a .catch) that produces a logged, typed outcome. An unhandled rejection propagating out of a loader on a key page is a violation (see AUDIT-RES-01).

AUDIT-RES-03 — Do not mask recurring failures behind empty success responses

Severity: important — Detection: runtime + manual

Rule: Fallback code paths (a try/catch returning empty data, an ErrorBoundary rendering a soft message) must log the underlying error with enough context, and recurring occurrences must be observable — a page whose backend is down must not silently return HTTP 200 with empty content.

Why: A real project served 200-with-empty-body responses while its backend was down: monitoring saw a healthy site, users saw empty pages, and the outage went unnoticed. Graceful degradation without observability turns incidents into invisible, long-running failures — and empty 200 pages can be cached by the CDN, prolonging the outage after recovery.

How to check:

  1. Review every catch block in route loaders and resolvers that returns a default value. Each must call the logger (logger.error(...) or equivalent) with the caught error. A catch that only returns json({ items: [] }) is a violation.

  2. Review ErrorBoundary exports in app/routes/: the boundary must be a last-resort display, not the primary handler of a known recurring error. Manual judgment: check the logs of a running environment for errors that fire on every request — an error boundary or catch firing constantly is a masked outage.

  3. Runtime: stop or firewall the e-commerce backend, then request key pages. Findings: a 200 status with empty product/category content, or a cacheable Cache-Control header on the degraded response.

AUDIT-RES-04 — Wire health checks for critical services to maintenance mode

Severity: important — Detection: static

Rule: Every service whose outage makes the storefront unusable (the e-commerce backend at minimum) must have a health check registered through services.MaintenanceMode.addHealthCheckService, so the store automatically enters and exits maintenance mode with the service.

Why: Without health checks, a backend outage exposes users to broken checkout flows and error pages until a human intervenes. Front-Commerce ships automatic maintenance-mode switching; not wiring it means paying for outages in customer trust instead of a maintenance page.

How to check:

  1. Grep for registrations:

    grep -rn "addHealthCheckService" extensions/ app/

    Zero occurrences in a project with a remote e-commerce backend is a finding. When occurrences exist, verify each critical dependency (backend, payment-critical services) is covered.

  2. Verify the maintenance mode API is enabled: the deployment must define FRONT_COMMERCE_MAINTENANCE_MODE_AUTHORIZATION_TOKEN (check .env.dist and deployment values).

  3. Check the health check schedule in front-commerce.config.ts (maintenance.healthChecks.schedule): the default is every 10 seconds; a custom Cron pattern slower than a few minutes defeats the purpose.

AUDIT-RES-05 — Never swallow errors

Severity: important — Detection: static

Rule: No catch block may be empty, and no catch block may return null/undefined/a default without logging the error. Fire-and-forget promises must carry a .catch that logs.

Why: Swallowed errors are the raw material of unexplainable production incidents: data silently missing, features silently off, and no trace to debug from. Every swallowed error also hides the early warning that would have prevented a bigger outage (see AUDIT-RES-03).

How to check:

  1. Find empty catches mechanically:

    grep -rEn "catch\s*(\([^)]*\))?\s*\{\s*\}" app/ extensions/ --include="*.ts" --include="*.tsx"

    Enable the ESLint no-empty rule (with allowEmptyCatch: false) for a repeatable pass.

  2. Find silent fallbacks: list all catch blocks, then flag those whose body contains a return but no logger call:

    grep -rn -A 4 "catch" app/ extensions/ --include="*.ts" --include="*.tsx" | grep -B 2 "return null\|return \[\]\|return undefined"

    Each flagged block without a logger. (or equivalent) statement is a violation.

  3. Review void somePromise() / un-awaited promise calls for a missing .catch — an unhandled rejection can crash the Node.js process.

AUDIT-RES-06 — Bound every list-accepting mutation

Severity: critical — Detection: static

Rule: Every GraphQL mutation or HTTP endpoint that accepts a list of items (bulk add-to-cart, quick order, batch updates, log ingestion) must enforce a maximum list length in the resolver, rejecting oversized inputs with a user error.

Why: Unbounded list inputs translate into unbounded loops and upstream calls: a single crafted request with thousands of items can saturate both Front-Commerce and the backend. This is simultaneously a performance bug and a denial-of-service vector reachable by any anonymous user.

How to check:

  1. Find list-accepting mutations in the schema:

    grep -rn "input\|Mutation" extensions/ --include="*.ts" | grep -E "\[\w+!?\]!?"

    Also review custom HTTP endpoints in app/routes/api.* that parse array bodies.

  2. For each one, read the resolver or action: there must be an explicit guard (if (items.length > MAX) throw ...) before any iteration or upstream call. A .map or loop over the input with no prior length check is a violation.

  3. Check that the limit is enforced server-side, not only in the UI — a frontend-only limit does not count.