jamiepine/voicebox · error · HTTPException

Invalid timecode update request

Error message

Invalid timecode update request

What it means

Returned by PUT /stories/{story_id}/items/times when stories.update_story_item_times returns falsy (HTTP 400, not 404). The handler treats a falsy update as a malformed/invalid request rather than a not-found, signaling that the StoryItemBatchUpdate payload failed validation or referenced items in an inconsistent way.

Source

Thrown at backend/routes/stories.py:107

    db: Session = Depends(get_db),
):
    """Remove a story item from a story."""
    success = await stories.remove_item_from_story(story_id, item_id, db)
    if not success:
        raise HTTPException(status_code=404, detail="Story item not found")
    return {"message": "Item removed successfully"}


@router.put("/stories/{story_id}/items/times")
async def update_story_item_times(
    story_id: str,
    data: models.StoryItemBatchUpdate,
    db: Session = Depends(get_db),
):
    """Update story item timecodes."""
    success = await stories.update_story_item_times(story_id, data, db)
    if not success:
        raise HTTPException(status_code=400, detail="Invalid timecode update request")
    return {"message": "Item timecodes updated successfully"}


@router.put("/stories/{story_id}/items/reorder", response_model=list[models.StoryItemDetail])
async def reorder_story_items(
    story_id: str,
    data: models.StoryItemReorder,
    db: Session = Depends(get_db),
):
    """Reorder story items and recalculate timecodes."""
    items = await stories.reorder_story_items(story_id, data.generation_ids, db)
    if items is None:
        raise HTTPException(
            status_code=400, detail="Invalid reorder request - ensure all generation IDs belong to this story"
        )
    return items

View on GitHub (pinned to 51f49dea19)

Solutions

  1. Validate that every item_id in the batch belongs to the story (via the detail endpoint) before submitting.
  2. Ensure timecodes are non-negative, ordered, and within each clip's bounds.
  3. Reject empty batches client-side.
  4. Re-fetch story detail after concurrent edits and rebuild the batch.

Example fix

// before
await api.put(`/stories/${sid}/items/times`, batch);

// after
if (!batch.length) throw new UserError('empty batch');
ensureTimesValid(batch, storyDetail);
await api.put(`/stories/${sid}/items/times`, batch);
Defensive patterns

Strategy: validation

Validate before calling

function validateTimeBatch(batch, storyItems) {
  if (!Array.isArray(batch) || !batch.length) throw new Error('empty batch');
  const known = new Set(storyItems.map(i => i.id));
  for (const b of batch) {
    if (!known.has(b.item_id)) throw new Error(`unknown item ${b.item_id}`);
    if (b.start < 0 || b.end < 0 || b.start >= b.end) throw new Error(`bad timecodes for ${b.item_id}`);
  }
}

Type guard

function isTimeBatch(v) { return Array.isArray(v) && v.every(b => typeof b.item_id === 'string' && typeof b.start === 'number' && typeof b.end === 'number'); }

Try / catch

try { await api.put(`/stories/${sid}/items/times`, batch); }
catch (e) { if (e.response?.status === 400) { await refreshStory(sid); showUser(e.response.data.detail); } throw e; }

Prevention

When it happens

Trigger: Submitting timecode updates where the referenced item ids don't match the story, timecodes are negative/out-of-order, or the batch is empty/malformed. The service returns falsy for any precondition failure during the batch update.

Common situations: Editor sends a batch with item ids from a different story. Timecodes computed client-side exceed clip duration. Empty batch submitted on save. Concurrent edits invalidate the batch.

Related errors


AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12). Data as JSON: /api/errors/02516b0dd06408bc. Report an issue: GitHub.