{"record":{"id":"02516b0dd06408bc","repo":"jamiepine/voicebox","slug":"invalid-timecode-update-request","errorCode":null,"errorMessage":"Invalid timecode update request","messagePattern":"Invalid timecode update request","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"backend/routes/stories.py","lineNumber":107,"sourceCode":"    db: Session = Depends(get_db),\n):\n    \"\"\"Remove a story item from a story.\"\"\"\n    success = await stories.remove_item_from_story(story_id, item_id, db)\n    if not success:\n        raise HTTPException(status_code=404, detail=\"Story item not found\")\n    return {\"message\": \"Item removed successfully\"}\n\n\n@router.put(\"/stories/{story_id}/items/times\")\nasync def update_story_item_times(\n    story_id: str,\n    data: models.StoryItemBatchUpdate,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Update story item timecodes.\"\"\"\n    success = await stories.update_story_item_times(story_id, data, db)\n    if not success:\n        raise HTTPException(status_code=400, detail=\"Invalid timecode update request\")\n    return {\"message\": \"Item timecodes updated successfully\"}\n\n\n@router.put(\"/stories/{story_id}/items/reorder\", response_model=list[models.StoryItemDetail])\nasync def reorder_story_items(\n    story_id: str,\n    data: models.StoryItemReorder,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Reorder story items and recalculate timecodes.\"\"\"\n    items = await stories.reorder_story_items(story_id, data.generation_ids, db)\n    if items is None:\n        raise HTTPException(\n            status_code=400, detail=\"Invalid reorder request - ensure all generation IDs belong to this story\"\n        )\n    return items\n\n","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/stories.py#L89-L125","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Validate that every item_id in the batch belongs to the story (via the detail endpoint) before submitting.","Ensure timecodes are non-negative, ordered, and within each clip's bounds.","Reject empty batches client-side.","Re-fetch story detail after concurrent edits and rebuild the batch."],"exampleFix":"// before\nawait api.put(`/stories/${sid}/items/times`, batch);\n\n// after\nif (!batch.length) throw new UserError('empty batch');\nensureTimesValid(batch, storyDetail);\nawait api.put(`/stories/${sid}/items/times`, batch);","handlingStrategy":"validation","validationCode":"function validateTimeBatch(batch, storyItems) {\n  if (!Array.isArray(batch) || !batch.length) throw new Error('empty batch');\n  const known = new Set(storyItems.map(i => i.id));\n  for (const b of batch) {\n    if (!known.has(b.item_id)) throw new Error(`unknown item ${b.item_id}`);\n    if (b.start < 0 || b.end < 0 || b.start >= b.end) throw new Error(`bad timecodes for ${b.item_id}`);\n  }\n}","typeGuard":"function isTimeBatch(v) { return Array.isArray(v) && v.every(b => typeof b.item_id === 'string' && typeof b.start === 'number' && typeof b.end === 'number'); }","tryCatchPattern":"try { await api.put(`/stories/${sid}/items/times`, batch); }\ncatch (e) { if (e.response?.status === 400) { await refreshStory(sid); showUser(e.response.data.detail); } throw e; }","preventionTips":["Ensure every item_id in the batch belongs to the story.","Validate timecodes are non-negative, ordered, and within clip bounds.","Reject empty batches client-side.","Rebuild the batch after concurrent edits."],"tags":["fastapi","http","validation","stories","timecode","rest"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}