Next.js 16.3.8 route handler: a GET-only Sume job status proxy

A Next.js route handler that proxies only GET /v1/jobs/{id}/status to Sume, validates the id and keeps the key server-side. Written for 16.3.8.

5 min readSume
All posts

To show Sume job progress in a browser without exposing your key, add one route handler that forwards a single read, GET /v1/jobs/{id}/status, and nothing else. The Next.js September 2026 security release, which shipped fixes in 16.3.8 and 15.5.27 (Next.js September 2026 security release, read 2026-10-04), is a good moment to write it narrowly: a proxy that forwards whatever path it is given is the kind of surface an advisory eventually describes.

The handler below validates the job id, sends exactly one credential header, and marks the response as not cacheable. Sume documents x-api-key and Bearer as equivalent, and a request carrying both is rejected with a 401 (Authentication).

Why the allowlist is the point

A catch-all proxy such as /api/sume/[...path] lets a visitor choose the upstream path, the method and sometimes the headers. Your key then authorizes whatever they pick: submits that spend credits, reads of other jobs in the workspace, key listing if the scope allows it. A fixed route with one id parameter removes those choices.

The release notes also describe a cache poisoning issue for apps that combine a root-level catch-all page with statically generated or ISR routes. If you have such a page, keep the proxy under its own path segment and do not make job status a statically generated route.

What the proxy accepts (design choices, Sume docs and Next.js release notes read 2026-10-04)
InputAcceptedReason
MethodGET onlyNo submit or cancel from the browser
Path/api/sume-status/[id]Fixed upstream path, no wildcard
Job idLetters, digits, dash, underscore, up to 80Blocks path tricks like .. and slashes
Credentialx-api-key from the server envOne header, never forwarded from the client
Cachingno-storeStatus changes while a job runs

The route handler

Save it as app/api/sume-status/[id]/route.ts. It reads the key from the server environment and fails with a 500 when it is missing, so a misconfigured deploy is loud.

const ID = /^[A-Za-z0-9_-]{1,80}$/;

export async function GET(
  _req: Request,
  ctx: { params: Promise<{ id: string }> },
) {
  const { id } = await ctx.params;
  const key = process.env.SUME_API_KEY ?? "";
  if (!key) return Response.json({ error: "server not configured" }, { status: 500 });
  if (!ID.test(id)) return Response.json({ error: "bad job id" }, { status: 400 });

  const upstream = await fetch(`https://api.sume.com/v1/jobs/${id}/status`, {
    headers: { "x-api-key": key },
    cache: "no-store",
  });
  const body = await upstream.text();
  return new Response(body, {
    status: upstream.status,
    headers: {
      "content-type": upstream.headers.get("content-type") ?? "application/json",
      "cache-control": "no-store",
    },
  });
}

Before you ship it

  • Add your own authentication so only the user who started a job can read its status. The key is workspace-wide, so the proxy is the only access check.
  • Return the upstream request id to the browser or log it. The error envelope carries request_id.
  • Pass the upstream status through unchanged, including 429, so your client can honour retry-after.
  • Upgrade to 16.3.8 or 15.5.27 as the release notes advise.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume