{"record":{"id":"2d0566b77838b3a4","repo":"jamiepine/voicebox","slug":"story-item-or-version-not-found","errorCode":null,"errorMessage":"Story item or version not found","messagePattern":"Story item or version not found","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"error","filePath":"backend/routes/stories.py","lineNumber":205,"sourceCode":"):\n    \"\"\"Duplicate a story item.\"\"\"\n    item = await stories.duplicate_story_item(story_id, item_id, db)\n    if item is None:\n        raise HTTPException(status_code=404, detail=\"Story item not found\")\n    return item\n\n\n@router.put(\"/stories/{story_id}/items/{item_id}/version\", response_model=models.StoryItemDetail)\nasync def set_story_item_version(\n    story_id: str,\n    item_id: str,\n    data: models.StoryItemVersionUpdate,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Pin a story item to a specific generation version.\"\"\"\n    item = await stories.set_story_item_version(story_id, item_id, data, db)\n    if item is None:\n        raise HTTPException(status_code=404, detail=\"Story item or version not found\")\n    return item\n\n\n@router.get(\"/stories/{story_id}/export-audio\")\nasync def export_story_audio(\n    story_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Export story as single mixed audio file.\"\"\"\n    try:\n        story = db.query(database.Story).filter_by(id=story_id).first()\n        if not story:\n            raise HTTPException(status_code=404, detail=\"Story not found\")\n\n        audio_bytes = await stories.export_story_audio(story_id, db)\n        if not audio_bytes:\n            raise HTTPException(status_code=400, detail=\"Story has no audio items\")\n","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/stories.py#L187-L223","documentation":"Raised (HTTP 404) by PUT /stories/{story_id}/items/{item_id}/version when stories.set_story_item_version returns None. The service returns None when the item is missing, its generation is missing, or the supplied version_id does not belong to the item's generation_id (DBGenerationVersion lookup filtered by both id and generation_id fails). The message covers all three cases.","triggerScenarios":"PUT .../version with a version_id from a different generation, a version_id that was deleted, an item_id/story_id mismatch, or pinning a version after the generation's version rows were regenerated.","commonSituations":"UI lists version options from one generation but the item was since re-pointed to another generation; client caches version_id across a regeneration that dropped old versions.","solutions":["Re-fetch the available versions for the item's current generation_id before pinning.","To unpin, send version_id = null/None — that path skips the version lookup and only needs a valid item.","Verify item_id belongs to story_id (GET /stories/{story_id}) if the 404 is unexpected."],"exampleFix":"// before\nawait api.setItemVersion(storyId, itemId, { version_id: cachedVersionId });\n\n// after\nconst versions = await api.listGenerationVersions(item.generation_id);\nif (!versions.some(v => v.id === cachedVersionId)) {\n  throw new Error('version no longer available for this generation');\n}\nawait api.setItemVersion(storyId, itemId, { version_id: cachedVersionId });","handlingStrategy":"validation","validationCode":"async function pinVersionIfAvailable(api, storyId, itemId, generationId, versionId) {\n  if (versionId == null) {\n    return api.setItemVersion(storyId, itemId, { version_id: null }); // unpin always allowed\n  }\n  const versions = await api.listGenerationVersions(generationId);\n  if (!versions.some(v => v.id === versionId)) {\n    throw new Error('version not available for this generation');\n  }\n  return api.setItemVersion(storyId, itemId, { version_id: versionId });\n}","typeGuard":"function isPinnableVersion(versions, versionId, item) {\n  return Array.isArray(versions)\n    && versions.some(v => v.id === versionId && v.generation_id === item.generation_id);\n}","tryCatchPattern":"try { await api.setItemVersion(storyId, itemId, payload); }\ncatch (e) {\n  if (e.status === 404) { await refreshItem(storyId, itemId); notify('Item or version not available'); }\n  else throw e;\n}","preventionTips":["Re-fetch version options for the item's current generation before pinning.","Send version_id=null to unpin — it skips the version lookup.","Invalidate cached version ids whenever the generation is regenerated."],"tags":["api","stories","fastapi","not-found","validation"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}