Problem
Three app-playground pages fetch https://jsonplaceholder.typicode.com — a free, unaffiliated public API — and two of them do it during next build:
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.
- 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
- Use it from all three pages.
- 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.
Problem
Three
app-playgroundpages fetchhttps://jsonplaceholder.typicode.com— a free, unaffiliated public API — and two of them do it duringnext build:app/isr/[id]/page.tsxgenerateStaticParams→ ids 1–3) + revalidationapp/ssg/[id]/page.tsxgenerateStaticParams→ ids 1–2) + on-demandapp/ssr/[id]/page.tsxcache: 'no-store')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):which surfaces as
Error: Local build failedfromsrc/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-cntnrsjust drew the short straw.Why not just drop the
fetchThe
fetchis load-bearing for what these tests actually cover.app/isr/[id]passesnext: { revalidate: 10, tags: ['collection'] }, so the call createsFETCH-kind entries in the incremental cache and is what exercises the custom cache handler's fetch-cache path plus tag invalidation viaapp/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.
examples/app-playground/lib/fetch-post.ts:RequestInit(includingnext: { revalidate, tags }) unchanged, so cache keys and tags are untouchedidrather than throwingexport const revalidate = 10toapp/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, andisr.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.tsandssr.test.tscompareRenderingInfotimestamps 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:5does a module-scopefetch(new URL('./Inter-SemiBold.ttf', import.meta.url)), which logs on every run, including passing ones:Reading the font with
fs.readFileSyncinside the handler clears it and makes real failures easier to spot in e2e logs.Scope
examples/only — no change to published construct code.