Skip to content

test(examples): app-playground pages fetch a third-party API at build time, flaking all e2e jobs #269

Description

@bestickley

Problem

Three app-playground pages fetch https://jsonplaceholder.typicode.com — a free, unaffiliated public API — and two of them do it during next build:

Page When the fetch runs Failure mode
app/isr/[id]/page.tsx build (generateStaticParams → ids 1–3) + revalidation aborts the build
app/ssg/[id]/page.tsx build (generateStaticParams → ids 1–2) + on-demand aborts the build
app/ssr/[id]/page.tsx request time (cache: 'no-store') 500s the page

There's no retry and no fallback, so a single TCP reset from that API takes down the whole e2e job. Seen in e2e run 35613527313 (rgnl-cntnrs):

[Error: aborted] { code: 'ECONNRESET' }
  ⨯ Failed to fetch https://jsonplaceholder.typicode.com/posts/2
Export encountered an error on /isr/[id]/page: /isr/2, exiting the build.

which surfaces as Error: Local build failed from src/nextjs-build/nextjs-build.ts:206. Re-running the same commit unchanged passed, confirming it was purely transient. All four e2e jobs are exposed to this — rgnl-cntnrs just drew the short straw.

Why not just drop the fetch

The fetch is load-bearing for what these tests actually cover. app/isr/[id] passes next: { revalidate: 10, tags: ['collection'] }, so the call creates FETCH-kind entries in the incremental cache and is what exercises the custom cache handler's fetch-cache path plus tag invalidation via app/api/revalidate. Replacing it with local data would silently delete that coverage.

Pointing it at a route handler in the same app doesn't work either — nothing is listening during next build.

Proposed fix

Make the call resilient instead of removing it.

  1. Add examples/app-playground/lib/fetch-post.ts:
    • retry a few times with short backoff
    • forward the caller's RequestInit (including next: { revalidate, tags }) unchanged, so cache keys and tags are untouched
    • on total failure, return deterministic local fallback content keyed by id rather than throwing
  2. Use it from all three pages.
  3. Also add export const revalidate = 10 to app/isr/[id]/page.tsx. That page currently derives its revalidation window from the fetch option. If the fetch fails and the fallback returns, no revalidate is registered, the page is cached indefinitely, and isr.test.ts → "should revalidate after 10 seconds" fails in a far more confusing way than a red build. Declaring it on the segment keeps ISR semantics independent of the fetch outcome.

A fallback is safe for the assertions as they stand: isr.test.ts, ssg.test.ts and ssr.test.ts compare RenderingInfo timestamps and never assert on the post title or body.

Unrelated noise worth fixing in the same pass

examples/app-playground/app/api/og/route.tsx:5 does a module-scope fetch(new URL('./Inter-SemiBold.ttf', import.meta.url)), which logs on every run, including passing ones:

TypeError: fetch failed ... [cause]: Error: not implemented... yet...

Reading the font with fs.readFileSync inside the handler clears it and makes real failures easier to spot in e2e logs.

Scope

examples/ only — no change to published construct code.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions