Genace

API request timeout: recover a generation job without resubmitting

·

An API request timeout describes the connection you were waiting on. It does not, by itself, describe the generation outcome. In Genace, submitting image or video generation and collecting its result are separate operations. Treating every timeout as permission to submit again can create two jobs and two credit deductions for one user request.

Identify which clock expired

Start with the operation recorded in your application log. A timeout during the generation POST is ambiguous: the server might have accepted the work before the connection disappeared. A timeout while reading an existing job is easier to recover from because you already have the job identifier.

OperationWhat expiration meansSafe next action
Submission POSTAcceptance may be unknownReconcile the attempt before another submission
Job GET with a wait windowThe read connection endedRead the same identifier again
Your workflow deadlineYour product stopped waitingSave the identifier for later collection
Terminal failed stateThe job finished unsuccessfullyInspect the structured job error

The job endpoint accepts a wait value clamped between zero and sixty seconds. This is a read window, not a promise that the model finishes within that time. The polling helper checks its elapsed time after status work, so the wait parameter is not a strict end-to-end HTTP deadline. It may return a queued or running snapshot when its window expires. Host limits and your client deadline remain separate constraints.

Give the read connection room

The following Node.js helper reads an existing job once. Missing configuration must fail before HTTP: interpolating an unset environment variable otherwise produces /jobs/undefined and a misleading authentication or not-found error. The helper performs no generation submission or cancellation.

export async function readGenerationJob({ jobId, apiKey }) {
  if (typeof jobId !== 'string' || !jobId.trim()) {
    throw new Error('A saved generation job ID is required');
  }
  if (typeof apiKey !== 'string' || !apiKey.trim()) {
    throw new Error('A server-side API key is required');
  }
  const response = await fetch(
    `https://genace.ai/api/v1/jobs/${encodeURIComponent(jobId)}?wait=30`,
    {
      method: 'GET',
      headers: { Authorization: `Bearer ${apiKey}` },
      signal: AbortSignal.timeout(45000),
    }
  );
  if (!response.ok) throw new Error(`Job read failed: ${response.status}`);
  const job = await response.json();
  const states = ['queued', 'running', 'succeeded', 'failed', 'canceled'];
  if (!job || job.id !== jobId || !states.includes(job.state)) {
    throw new Error('Invalid job snapshot');
  }
  return job;
}

Save the helper in a server-side ES module, then call it with your saved identity and credential:

const job = await readGenerationJob({
  jobId: process.env.GENACE_JOB_ID,
  apiKey: process.env.GENACE_API_KEY,
});
console.log({ id: job.id, state: job.state, outputs: job.outputs });

Allowing more than thirty seconds for a thirty-second long poll leaves space for authentication, network transfer, and upstream status work. The example’s forty-five-second deadline is an application choice, not a service guarantee. If your hosting platform allows only ten-second requests, reduce the wait window instead of copying these values unchanged.

Recover without inventing failure

Persist the POST response’s job_id immediately. Subsequent GET responses use id, so normalize that difference at your application boundary. Store a local status such as waiting_for_collection when your overall deadline expires. Keep that separate from the remote failed state, which only the job response can establish.

A received 502 can also contain a useful local ID at error.job_id. For UPSTREAM_SUBMISSION_UNCERTAIN, retain that ID and the credit hold for reconciliation; do not automatically submit again or cancel to force a refund. UPSTREAM_SUBMISSION_FAILED reports an explicit rejection, but compensation can still need retry, so inspect that same local record. A disconnected POST with no response or a generic 504 is not evidence of rejection. See submission uncertainty.

When AbortSignal.timeout expires, fetch can reject with TimeoutError; aborting locally does not establish remote cancellation. The helper deliberately lets observation exceptions propagate rather than fabricating a failed job. Handle these exceptions in your application by keeping the original ID available for a later GET. Do not treat every error named AbortError or every HTTP 5xx as safe permission to recreate work.

Common mistakes include setting the client timeout equal to the wait value, deleting the job record after a socket error, and displaying zero progress as proof that work stopped. Progress can be null. None of these observations proves a failed render or a refund.

Verification checklist

Before using a real job, replace fetch with offline fixtures: missing ID or key must cause zero requests; each valid snapshot must cause one GET to the encoded saved ID; a transport timeout must propagate without retry or cancellation. Reject a mismatched response ID and an invented pending state. Exercise all five actual states, including failed with HTTP 200 and null outputs, without confusing transport success with generation success.

Continue with resuming saved jobs, handling ambiguous duplicate requests, and polling correctly.

Continue with production operations

Where these facts come from