{"record":{"id":"162d137fff39181b","repo":"jamiepine/voicebox","slug":"invalid-reorder-request-ensure-all-generation-id","errorCode":null,"errorMessage":"Invalid reorder request - ensure all generation IDs belong to this story","messagePattern":"Invalid reorder request - ensure all generation IDs belong to this story","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"backend/routes/stories.py","lineNumber":120,"sourceCode":"    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\n@router.put(\"/stories/{story_id}/items/{item_id}/move\", response_model=models.StoryItemDetail)\nasync def move_story_item(\n    story_id: str,\n    item_id: str,\n    data: models.StoryItemMove,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Move a story item (update position and/or track).\"\"\"\n    item = await stories.move_story_item(story_id, item_id, data, db)\n    if item is None:\n        raise HTTPException(status_code=404, detail=\"Story item not found\")\n    return item\n","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/stories.py#L102-L138","documentation":"Returned by PUT /stories/{story_id}/items/reorder when stories.reorder_story_items returns None (HTTP 400). The handler uses None (distinguished from an empty list) to signal that the supplied generation_ids do not all belong to the story, so the reorder is rejected entirely — partial reorders are not applied.","triggerScenarios":"Reorder request whose generation_ids include an id not in the story, or omit some/all current items. The service validates the set matches the story's items and returns None on mismatch.","commonSituations":"Client reorder list built from a stale story view. Split/trim created new item ids not reflected in the client. Drag-and-drop UI that dropped an item from the ordering.","solutions":["Rebuild the full ordered generation_ids list from the current GET /stories/{story_id} detail before reordering.","Ensure the reorder payload is a permutation of exactly the story's current item generation ids (no missing, no extra).","After any split/trim/move, refresh the detail so ids are current.","Validate set equality client-side: same set as the story's items."],"exampleFix":"// before\nawait api.put(`/stories/${sid}/items/reorder`, { generation_ids: order });\n\n// after\nconst current = (await getStory(sid)).items.map(i => i.generation_id);\nif (new Set([...order, ...current]).size !== current.length) throw new Error('order set mismatch');\nawait api.put(`/stories/${sid}/items/reorder`, { generation_ids: order });","handlingStrategy":"validation","validationCode":"function validateReorder(order, storyItems) {\n  const current = storyItems.map(i => i.generation_id).sort();\n  const sent = [...order].sort();\n  if (current.length !== sent.length) throw new Error('count mismatch');\n  for (let i = 0; i < current.length; i++) if (current[i] !== sent[i]) throw new Error('set mismatch');\n}","typeGuard":"function isReorder(v) { return Array.isArray(v) && v.every(x => typeof x === 'string'); }","tryCatchPattern":"try { await api.put(`/stories/${sid}/items/reorder`, { generation_ids: order }); }\ncatch (e) { if (e.response?.status === 400) { await refreshStory(sid); rebuildOrder(); } throw e; }","preventionTips":["Rebuild the full ordered list from the current story detail before reordering.","Ensure the payload is a permutation of exactly the story's current generation ids.","Refresh detail after split/trim/move so ids are current."],"tags":["fastapi","http","validation","stories","reorder","rest"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}