Next.js 16.3.8 fixes ISR cache poisoning: keep job pages dynamic

Next.js v16.3.8 fixes cache poisoning in SSG and ISR and Draft Mode leaks. Why a Sume job status page should stay dynamic and uncached, whatever the version.

4 min readSume
All posts

The Next.js releases page lists v16.3.8, printed on September 30, as fixing cache poisoning in SSG and ISR and leaks in Draft Mode, alongside an SSRF fix in Image Optimization and a low-severity dev-server MCP disclosure. If you show a Sume render's progress on a Next.js page, keep that page dynamic and uncached, and upgrade to the patched release.

A job status is per-request, changing data. It is the wrong thing to put behind a static or incremental cache even on a version with no known bug.

What the release entry lists

Severities are as the page printed them. I read the page on 2026-10-03 and did not read the advisories behind each line, so check them for affected versions.

Next.js v16.3.8 fixes (read 2026-10-03)
AreaIssueSeverity on the page
Image OptimizationSSRFHigh
SSG and ISRCache poisoningNot stated in the entry read
Draft ModeLeaksNot stated in the entry read
Dev server MCPInformation disclosureLow

Why job status should never be cached

Sume's docs say a job is created as a durable record and that you poll status_url until terminal is true, then read result_url. Status moves from queued to processing to a terminal value, so a cached copy shows the wrong state, and in the worst case shows one user's job to another. Cache poisoning bugs are a reminder that shared caches are an attack surface; the safest status page is one the cache never sees.

A second reason is that a result belongs to one job, so a statically generated page that embeds a result URL at build time is a pattern to avoid.

A dynamic route that polls on the server

This route handler forces dynamic rendering, reads the job status with the key held on the server, and sends the browser no-store. It uses GET /v1/jobs/:id/status from the Sume API reference.

// app/api/jobs/[id]/status/route.ts
export const dynamic = "force-dynamic";

export async function GET(
  _req: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const key = process.env.SUME_API_KEY;
  if (!key) return new Response("missing key", { status: 500 });
  const r = await fetch(
    `https://api.sume.com/v1/jobs/${encodeURIComponent(id)}/status`,
    { headers: { Authorization: `Bearer ${key}` }, cache: "no-store" }
  );
  return new Response(await r.text(), {
    status: r.status,
    headers: {
      "content-type": "application/json",
      "cache-control": "no-store",
    },
  });
}

Webhook handlers are dynamic too

A webhook receiver should never be cached either, and it should read the raw body, because Sume signs the raw JSON body with HMAC SHA 256 over <timestamp>.<raw_body>. Return a 2xx only after storing the event, and treat job_id as your idempotency key, as the webhook docs state. Keep status_url polling as a backup for deliveries that never arrive.

  • Upgrade to v16.3.8 or later and read the advisories for your affected range.
  • Mark job status and webhook routes dynamic and send cache-control: no-store.
  • Keep the Sume API key in server environment variables, never in a client bundle.
  • Never render a result URL at build time.

Upgrading safely

After you upgrade, test the status route with two different job ids from two different sessions and confirm each response matches its own job. That check is cheap and catches the exact class of bug a poisoned cache would produce. Also confirm that no page imports the API key into client code, because a dynamic route does not help if the key ships in a bundle.

If you proxy generated images through Next.js image optimization, the same release lists an SSRF fix there; keep your allowed remote patterns narrow and limited to the Sume media host you actually use.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume