Node 26.11 ships Undici 8.11.2: tell a Sume poll timeout from an abort

Catch TimeoutError separately from AbortError when a Sume status poll in Node fetch times out, and know which Sume calls are safe to repeat after that timeout.

4 min readSume
All posts

When a Sume status poll times out in Node, fetch rejects and the error's name tells you why: a deadline you set with AbortSignal.timeout() rejects with TimeoutError, while a signal that a caller aborted rejects with AbortError. Handle them separately. A poll timeout means ask again; a caller abort means stop. In a test on Node 22.14 a silent local server produced TimeoutError after the set delay, and a closed port produced a network error whose cause.code was ECONNREFUSED.

This week's Node.js 26.11.0 notes list Undici 8.11.2 among the updated dependencies, so Node 26 users are on a new fetch implementation. The pattern below does not depend on the version, but it is a good moment to add the test.

Which Sume calls are safe after a timeout

A timeout tells you only that you did not see a response. It does not tell you the server did nothing. For read calls that is harmless. For a paid submit it is the whole problem, which is why Sume asks you to repeat the submit with the same Idempotency-Key instead of sending a new job. The jobs and results docs describe these routes.

Sume job routes and what to do after a client timeout (read 2026-10-08)
RouteMethodAfter a timeout
/v1/jobs/{id}/statusGETSafe to repeat; honor next_poll_after_seconds
/v1/jobs/{id}/resultGETSafe to repeat; 409 job_not_completed means not finished yet
/v1/images or /v1/videos submitPOSTRepeat with the same Idempotency-Key, never a new one
/v1/jobs/{id}/cancelPOSTCheck status first; cancel works only before generation starts

Steps

Treat the per-request timeout and the overall job deadline as two different numbers. Video jobs can take minutes, but a single status read should answer in seconds. If one read hangs, you want to drop that connection quickly and ask again, while the clock for the whole job keeps ticking. Mixing the two is how a loop either gives up too early or waits forever on a dead socket.

  • Give every poll its own AbortSignal.timeout(), short enough that one stuck connection cannot eat your whole job deadline.
  • Branch on err.name. Return a retry hint for TimeoutError, stop for AbortError, and read err.cause?.code for network failures.
  • Keep a total deadline separate from the per-request one. A poll timeout should consume deadline, not reset it.
  • Authenticate with one credential. Sending both Authorization: Bearer and x-api-key returns a 401, as the authentication docs explain.

Sample

The function below returns a small result object instead of throwing, so a poll loop can decide what to do. It reads the key from SUME_API_KEY.

const base = process.env.SUME_BASE ?? "https://api.sume.com";
const auth = { Authorization: `Bearer ${process.env.SUME_API_KEY}` };

async function getStatus(jobId, ms = 10_000) {
  try {
    const res = await fetch(`${base}/v1/jobs/${jobId}/status`, {
      headers: auth,
      signal: AbortSignal.timeout(ms),
    });
    return { ok: res.ok, body: await res.json() };
  } catch (err) {
    if (err.name === "TimeoutError") return { retry: "poll timed out, safe to ask again" };
    if (err.name === "AbortError") return { retry: "aborted by a caller signal" };
    return { retry: `network error: ${err.cause?.code ?? err.message}` };
  }
}
const quick = await getStatus("job_demo", Number(process.env.POLL_MS ?? 10_000));
console.log(JSON.stringify(quick));

What Sume does not do

Sume does not return a special status when your client gives up. The job keeps running and keeps its status, so after a timeout you ask again by job id. Sume also does not cancel a job because your poll was aborted; cancel is a separate call that works only before generation starts.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume