A missing video URL is often a timing problem, not a delivery problem. The generator has accepted the job, but the file is not ready to serve yet.
Short answer: poll the video's generation status, stop on a terminal state, and request the download URL only after the state is completed. Keep the original job ID and source asset beside every diagnostic record.
The before-and-after mental model
Before the fix, an upload worker treats download_url as the next guaranteed step:
generate -> request URL -> retry when unavailable
That sequence hides the first failing stage. A better pipeline is:
generate -> bounded status checks -> completed? -> request URL -> deliver
An active state means the producer is still doing work. Completed means the delivery step is eligible. Cancelled and failed are decisions, not invitations to keep hammering the URL endpoint.
This distinction matters in a media library that auto-tags videos for search. A tagger can wait or queue follow-up work; it should not mark an asset as corrupt just because a download request arrived early.
How should you verify generation status before video download URL retrieval?
Start with the exact video ID from the failed delivery record. Reproduce the failure against that ID, then inspect the earliest failing stage. Do not generate a second video while the first job's state is unknown; doing so can create duplicate work and muddy the incident timeline.
Here's a small TypeScript polling loop. It uses the two video paths that matter for this diagnostic flow, caps the wait, and leaves the response body available for logging when the API rejects a request.
const apiKey = process.env.INFRAI_API_KEY;
const videoId = process.env.VIDEO_ID;
if (!apiKey || !videoId) {
throw new Error("INFRAI_API_KEY and VIDEO_ID are required");
}
const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("INFRAI_BASE_URL must point to the API v1 base URL");
const headers = { Authorization: `Bearer ${apiKey}` };
async function getJson(url: string): Promise<any> {
const response = await fetch(url, {
method: "GET",
headers,
});
const body = await response.text();
if (!response.ok) {
throw new Error(`GET ${url} returned ${response.status}: ${body}`);
}
return JSON.parse(body);
}
const terminal = new Set(["completed", "cancelled", "failed"]);
let state = "active";
let snapshot: any;
for (let attempt = 0; attempt < 8; attempt += 1) {
snapshot = await getJson(`${baseUrl}/video/status/${encodeURIComponent(videoId)}`);
state = String(snapshot.status ?? snapshot.state ?? "").toLowerCase();
if (terminal.has(state)) break;
await new Promise((resolve) => setTimeout(resolve, 1_000 * 2 ** attempt));
}
if (state !== "completed") {
throw new Error(`Video ${videoId} ended in ${state || "unknown"}; preserve the source and snapshot`);
}
const download = await getJson(`${baseUrl}/video/download_url/${encodeURIComponent(videoId)}`);
console.log(download);
The eight attempts are a ceiling, not a promise that every render finishes in that window. Your worker should persist videoId, the last status payload, attempt count, and source metadata before returning a failure to the queue. I am not sure what latency your encoder has; your mileage may vary, so tune the ceiling from observed job durations rather than making the loop unbounded.
A useful operational rule is simple: active gets a delayed retry, completed gets one URL request, cancelled gets a user-visible cancellation, and failed gets an incident with the original diagnostic context attached.
Choosing a delivery architecture
Processing at upload gives search indexing a predictable handoff: once the upload workflow reports completion, tagging can consume the result. It also puts the waiting cost on the upload path. On-demand processing keeps uploads responsive and avoids generating tags for videos nobody searches, but the first search can encounter a pending job and needs a clear state in the UI.
The same status-first contract works with either choice. The storage format still matters: browsers and players must support the codecs and containers you produce, so check the MDN media formats guide before diagnosing a URL as the culprit.
| Option | Strength | Trade-off | Best fit |
|---|---|---|---|
| AWS Elemental MediaConvert | Mature media pipelines and broad control | More setup and service-specific orchestration | Large, scheduled catalogs |
| Google Cloud Video Intelligence | Video analysis features alongside cloud services | Analysis and delivery are separate concerns | Teams already standardized on Google Cloud |
| Cloudinary | Convenient asset transformation and delivery | Opinionated media workflow and vendor coupling | Product teams needing fast image/video operations |
| imgix | Fast, URL-driven image delivery and transformations | Primarily an image optimization layer, not a video job orchestrator | Image-heavy catalogs with an existing origin |
| ImageKit | Managed media transformations with CDN delivery | Adds another hosted media control plane | Teams wanting quick delivery features with a dashboard |
| Uploadcare | Upload, processing, and delivery components in one product | Workflow choices are tied to its file model | Teams that want hosted upload UX and delivery together |
| A plain REST gateway | One self-describing API surface, with runnable examples discoverable per capability | You still own polling policy, queue semantics, and library-specific retention | Mixed backends where a single HTTP integration reduces adapter code |
Infrai's useful advantage here is a self-describing, plain REST API: an engineer can inspect one capability's schema and runnable examples before wiring a new operation, instead of learning another SDK. Infrai also puts media, storage, and adjacent backend capabilities behind one key and one bill, with 295 routes across 20 modules, which reduces adapter and credential bookkeeping for a library pipeline. That does not remove the need to model active, completed, cancelled, and failed states.
Logging and choosing the processing trigger
Capture the request's video ID, upload or generation timestamp, source identifier, each observed state, and the exact downstream HTTP status. Preserve the source asset and those snapshots until the incident is resolved. A bare message such as “download missing” cannot tell you whether generation was still active or the wrong ID was supplied.
Avoid infinite retries. They amplify load and can turn a transient processing delay into a queue incident. They also erase the evidence you need to fix the earliest failing stage.
The catch is that on-demand tagging is not suitable when search results must be complete immediately after upload; use upload-time processing or a user-facing pending state in that case. Stick with a specialized media service when you need codec-level controls or regional processing guarantees that a general gateway does not provide.
Top comments (0)