Skip to content

Say whether a failed /api/ build is drift or an outage; always build live - #257

Merged
mrshll merged 2 commits into
mainfrom
api-examples-snapshot
Sep 29, 2026
Merged

mrshll merged 2 commits into
mainfrom
api-examples-snapshot

Conversation

@mrshll

@mrshll mrshll commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

/api/ still renders live from api.dynamical.org on every build, and a documented request the API refuses still fails the build. This PR changes two things. When a build fails, it now says which kind of failure it saw. And "every build" now really means every build, with no cached response hiding a refusal.

What changes

  • Failures are classified by what the API did (classifyFailure and failureDetail in lib/api-examples.js). eleventy-fetch puts the Response in cause when the API answered, and the network error there when it didn't.
    • refused (4xx other than 408/429): fails at once with no retry, since asking again gets the same answer. The message carries the API's own detail, compacted to JSON when it's a FastAPI validation array, and capped at 500 characters. It says this usually means the documented request has drifted from the API. A refusal can also come from the edge, such as a WAF 403, so the detail is printed rather than assumed.
    • failed (5xx, 408, 429, timeout, no connection): gets the existing single retry, then fails with "the API failed rather than refusing the request, so this is not drift: rebuild once it recovers". The first run of today's 524 would have read that way.
    • other (for example a 200 whose body isn't JSON): says only that the example couldn't be fetched, rather than calling it an outage.
    • Every fetch-failure message names the example, method, path and base. The canonical example's follow-up GET used to escape the named-example message; it now reports under canonical.
  • Builds always ask the API. The cache duration is now 0s outside --serve/--watch, where it was 6h everywhere. It now reads ELEVENTY_RUN_MODE to tell them apart, and API_EXAMPLES_CACHE_DURATION still overrides it.
    • Why: when a fetch fails and the duration is positive, eleventy-fetch silently returns an expired cache entry. That includes a 422.
    • The Pages project has build caching on, and Eleventy's .cache is one of the directories it keeps between builds. So a deploy could render an old response over a request the API now refuses, and pass. That is the drift this build check exists to catch.
    • A 6h-fresh cache also skipped the API outright, so on main neither "every build renders live" nor "a refusal fails the build" held on Pages.
    • --serve keeps 6h so editing stays off the API.
    • Two consecutive npx @11ty/eleventy runs each made all 7 requests to api.dynamical.org, even with .cache warm. The quickstart and canonical still share one forecast POST: eleventy-fetch reuses an in-flight or settled request with the same key.
  • The live endpoint spec also checks the method. test/e2e/api-docs.spec.mjs now fails when a documented method isn't published at that path, not only when the path is missing.

Tests

  • test/api-docs.test.mjs (unit):
    • which statuses and errors are refused, failed or other
    • that detail extraction prefers the API's detail, keeps validation arrays, and falls back to the start of a non-JSON page or the bare status
    • "fetch failed" plus its underlying code
  • test/api-examples-loader.test.mjs (new; still offline, using a stand-in API on localhost with each run in its own process and cache dir, about 2 s):
    • a 422 is asked once and reported with its detail
    • a 524 is retried once, and recovers or fails as "not drift"
    • a failing canonical GET reports under canonical
    • a non-JSON 200 isn't called an outage
    • the cache hazard, with the analysis window pinned so runs either side of an hour share cache keys: under --serve, an expired cache papers over a 422 (asserted, so the test would notice if eleventy-fetch stopped doing that), and a build with the same .cache fails on that 422

Checks

  • npm test: 337 pass
  • test/e2e/api-docs.spec.mjs against the live API: 2 pass
  • the loader against a local 422 / 524 / closed port, and against production
  • two consecutive builds each make 7 live API requests
  • Codex review pass 3 (the first on this version): two findings, both fixed in the follow-up commit. The cache test could flake across a UTC hour, since the analysis body is part of the cache key. And this description overstated the detail handling and the reach of the request context.
  • Codex review pass 4: no findings; both fixes confirmed, including an offline probe across the hour boundary

Not in this PR

  • No committed capture, no date. The page still says its commands ran against the live API "when this page was built", with no date.
  • No per-request timeout. Main has none; a stuck request waits for the edge's own 524.
  • What still doesn't notice drift: a successful response that changes shape just renders the new shape. The prose checks in test/e2e/api-docs.spec.mjs only run with the whole e2e suite (daily and on scorecard/e2e PRs via scorecard-charts.yml), not on every PR.

2026-09-28: first version (snapshots)

The first version of this PR rendered /api/ from a committed, dated capture (lib/api-snapshot/). It checked that capture against the live API only on PRs touching the API docs, and there were two Codex review passes on that design. Marsh chose instead to keep every build live, accepting build and CI dependence on the API for drift detection, and to show no capture date.

2026-09-29: reworked for live builds

  • Branch reset to main and rebuilt as a fresh commit plus review fixes; the snapshot design survives only in this log.
  • Kept from it: the outage-vs-refusal reporting, the canonical follow-up's error context, and the method check (from its offline contract tests).
  • Found while reworking: the stale-cache fallback. It is the reason builds now use a 0s cache.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Deploying dynamical-org with  Cloudflare Pages  Cloudflare Pages

Latest commit: f1f0495
Status: ✅  Deploy successful!
Preview URL: https://bd61d2c1.dynamical-org.pages.dev
Branch Preview URL: https://api-examples-snapshot.dynamical-org.pages.dev

View logs

@mrshll mrshll assigned mrshll and unassigned mrshll Sep 28, 2026
…live

The loader rendered every failure the same way. It now classifies what it saw:
a refused request (4xx) fails at once with the API's own detail, as a likely sign
the documented request has drifted; a failing API (5xx, 408, 429, no connection)
is retried once and reported as not drift; anything else says only that the
example could not be fetched. The canonical follow-up gets the same treatment.

Builds no longer cache API responses. eleventy-fetch answers a failed request with
an expired cache entry whenever the duration is positive, and Pages keeps .cache
between builds, so a deploy could render an old response over a request the API
now refuses. --serve keeps its 6h cache.

The live endpoint spec also checks each documented method is published.
@mrshll
mrshll force-pushed the api-examples-snapshot branch from e6e212c to 9b0531b Compare September 29, 2026 02:42
@mrshll mrshll changed the title Render /api/ from a committed capture; check it live only on API-docs PRs Say whether a failed /api/ build is drift or an outage; always build live Sep 29, 2026
Review: each run builds the analysis request from its own clock, and its body is
part of the cache key, so runs straddling a UTC hour missed the cache for reasons
unrelated to the test. Also assert the stale run really reached the API.
@mrshll
mrshll marked this pull request as ready for review September 29, 2026 03:02
@mrshll mrshll self-assigned this Sep 29, 2026
@mrshll
mrshll merged commit 8ed747b into main Sep 29, 2026
7 checks passed
@mrshll
mrshll deleted the api-examples-snapshot branch September 29, 2026 03:12
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