Back to BlogSEO

One Enum Mismatch Hid Every Blog Video for Weeks

The Bug That Looked Like a Design Decision For weeks, dozens of blog posts on a HeyGen powered video platform showed a blank space where the video player…

Ryan Mayiras
Aug 19, 2026
typescriptmongodbnextjsheygen
One Enum Mismatch Hid Every Blog Video for Weeks

The Bug That Looked Like a Design Decision

For weeks, dozens of blog posts on a HeyGen-powered video platform showed a blank space where the video player should have been. No error. No broken image icon. Just nothing. The CDN MP4 existed. The HeyGen render had completed. The database had a valid URL. And yet: blank page.

This is the story of how a single mismatched status string caused every video to silently disappear, and exactly how we fixed it.

What HeyGen Sends vs. What We Expected

HeyGen's webhook payload includes a status field. When a video finishes rendering, that field arrives as 'completed'.

Our Next.js frontend was written to render the video player only when status === 'ready'.

Those two strings never matched. Not once.

Every time HeyGen fired a webhook, our handler wrote videoStatus: 'completed' into MongoDB. The Next.js page checked for 'ready', found something else, and rendered nothing. The player component never mounted. No console error, because the code path was technically correct. It just never reached the render branch.

The result: dozens of posts with a valid files2.heygen.ai MP4 URL sitting in the database and a blank

where the player should have been.

Mapping the Broken State Machine

The first step was writing down the actual states the system was moving through versus the states we intended.

Intended finite state machine:

none → pending → ready | failed

Actual states in MongoDB after weeks of webhook writes:

none → pending → completed

'ready' never appeared in the database. Not in a single document. The finalize function, the retry handler, and the webhook writer were all using different strings with no shared enum.

The Fix: One Source of Truth for Status

We defined a TypeScript enum that every writer had to import:

export enum VideoStatus {
  None = 'none',
  Pending = 'pending',
  Ready = 'ready',
  Failed = 'failed',
}

Then we updated three places that were writing status to MongoDB:

1. The HeyGen webhook handler

// Before
await db.collection('posts').updateOne(
  { heygenJobId: payload.video_id },
  { $set: { videoStatus: payload.status } } // wrote 'completed'
);

// After
const statusMap: Record<string, VideoStatus> = {
  completed: VideoStatus.Ready,
  failed: VideoStatus.Failed,
};

const normalized = statusMap[payload.status] ?? VideoStatus.Pending;

await db.collection('posts').updateOne(
  { heygenJobId: payload.video_id },
  { $set: { videoStatus: normalized } }
);

2. The finalize function (called after manual review) was updated to write VideoStatus.Ready instead of the string 'ready' it had been writing inconsistently.

3. The retry handler was writing 'pending' as a raw string. Updated to VideoStatus.Pending.

Bulk-Updating the Existing Documents

Fixing the writers only helped future webhooks. The existing documents in MongoDB still had videoStatus: 'completed' on dozens of posts. We ran a targeted update:

db.collection('posts').updateMany(
  { videoStatus: 'completed' },
  { $set: { videoStatus: 'ready' } }
);

After that query ran, every post with a valid CDN URL immediately became visible. The Next.js pages re-rendered on the next request and the players mounted correctly. No redeployment needed. No content changes. Just the status string corrected in the database.

Preventing Dead Players from Expired URLs

While we were in the webhook handler, we found a second problem. HeyGen's files2.heygen.ai URLs expire. If a post sat in 'completed' limbo long enough, the URL in the database was no longer valid. Rendering the player with a dead URL would show a broken player, which is worse than showing nothing because it signals to the reader that something went wrong.

We added a URL validation step before the player component mounts:

async function isVideoUrlAlive(url: string): Promise<boolean> {
  try {
    const res = await fetch(url, { method: 'HEAD' });
    return res.ok;
  } catch {
    return false;
  }
}

If the check returns false, the component renders a static thumbnail with a "Video processing" label instead of a broken player. The post still reads correctly. The reader is not confused.

This also means the system now rejects expired files2.heygen.ai URLs at render time rather than silently displaying a broken embed.

Why This Stayed Hidden So Long

A few things made this bug hard to catch:

  • No error was thrown. The conditional if (status === 'ready') evaluated to false and the component returned null. Perfectly valid React.
  • The database looked healthy. Documents had a videoStatus field with a value. Nothing was null or missing.
  • HeyGen's dashboard showed successful renders. The videos existed. The problem was entirely in how we stored and read the status.
  • The blank space looked intentional. Without a prior working state to compare against, it was easy to assume the player was just not yet wired up.
  • The only way we caught it was by querying MongoDB directly and noticing that 'ready' appeared zero times while 'completed' appeared dozens of times.

    The Broader Pattern

    This class of bug appears whenever two systems use the same concept with different vocabulary and there is no translation layer between them. HeyGen owns its status strings. We own ours. The webhook handler is the boundary, and it should have been doing the translation from day one.

    The fix is not complicated: define your internal states explicitly, map external values to them at the entry point, and never let a third-party string propagate into your own database. A TypeScript enum enforced at compile time would have caught this before it shipped.

    This is the kind of integration work the team at Savage Digital Solutions (savagesolutions.io) runs into regularly when connecting AI video tools to production content pipelines. The tools are capable. The gaps are almost always in the plumbing between them.

    Key Takeaways

  • HeyGen webhooks write status: 'completed'. If your app expects 'ready', every video silently disappears.
  • Define a TypeScript enum for your internal video states (none, pending, ready, failed) and import it everywhere status is written to MongoDB.
  • The webhook handler is the translation layer. Map third-party status strings to your internal enum at that boundary, not downstream.
  • After fixing the writers, run a updateMany to correct existing documents. Future webhooks will not fix historical data.
  • files2.heygen.ai URLs expire. Validate the URL with a HEAD request before mounting the player. Render a fallback rather than a broken embed.
  • A blank page with no error is often a conditional that evaluates to false, not a missing component. Query the database directly when the UI gives you nothing to debug.
Share this article:TwitterLinkedInFacebookReddit

Want to Learn More?

Explore more articles on workflow automation and digital transformation.

View All Articles