{"record":{"id":"69a0c74d31324f73","repo":"jamiepine/voicebox","slug":"generation-not-found-69a0c7","errorCode":null,"errorMessage":"Generation not found","messagePattern":"Generation not found","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"warning","filePath":"backend/routes/history.py","lineNumber":86,"sourceCode":"    count = await history.delete_failed_generations(db)\n    return {\"deleted\": count}\n\n\n@router.get(\"/history/{generation_id}\", response_model=models.HistoryResponse)\nasync def get_generation(\n    generation_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Get a generation by ID.\"\"\"\n    result = (\n        db.query(DBGeneration, DBVoiceProfile.name.label(\"profile_name\"))\n        .join(DBVoiceProfile, DBGeneration.profile_id == DBVoiceProfile.id)\n        .filter(DBGeneration.id == generation_id)\n        .first()\n    )\n\n    if not result:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n\n    gen, profile_name = result\n    return models.HistoryResponse(\n        id=gen.id,\n        profile_id=gen.profile_id,\n        profile_name=profile_name,\n        text=gen.text,\n        language=gen.language,\n        audio_path=gen.audio_path,\n        duration=gen.duration,\n        seed=gen.seed,\n        instruct=gen.instruct,\n        engine=gen.engine or \"qwen\",\n        model_size=gen.model_size,\n        status=gen.status or \"completed\",\n        error=gen.error,\n        is_favorited=bool(gen.is_favorited),\n        created_at=gen.created_at,","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/history.py#L68-L104","documentation":"Returned by GET /history/{generation_id} when the query `db.query(DBGeneration, DBVoiceProfile.name).join(DBVoiceProfile, ...).filter(DBGeneration.id == generation_id).first()` returns None. Because this uses an INNER JOIN against voice_profiles, a generation whose profile has been deleted will NOT be returned even though the generation row exists — the join eliminates it. HTTP 404.","triggerScenarios":"generation_id genuinely absent; OR generation exists but its voice_profiles row was deleted (orphaned generation), causing the INNER JOIN to drop it; profile_id NULL on the generation row.","commonSituations":"Profile deleted via /profiles/{id} without cascading or nulling the generation's profile_id, orphaning its generations from this join; referencing an old id after a DB reset.","solutions":["If the row might be orphaned, query the generations table directly (without the profile join) to confirm existence.","Use a LEFT OUTER JOIN instead of INNER JOIN in this query so orphaned generations still resolve (profile_name becomes None).","When deleting a profile, either cascade-delete its generations or reassign/null their profile_id consistently.","Confirm the generation_id is the exact UUID stored."],"exampleFix":"# before (inner join drops orphaned generations)\nresult = db.query(DBGeneration, DBVoiceProfile.name.label(\"profile_name\")) \\\n    .join(DBVoiceProfile, DBGeneration.profile_id == DBVoiceProfile.id) \\\n    .filter(DBGeneration.id == generation_id).first()\n\n# after (outer join keeps orphaned rows)\nresult = db.query(DBGeneration, DBVoiceProfile.name.label(\"profile_name\")) \\\n    .outerjoin(DBVoiceProfile, DBGeneration.profile_id == DBVoiceProfile.id) \\\n    .filter(DBGeneration.id == generation_id).first()","handlingStrategy":"validation","validationCode":"// Confirm existence (be aware INNER JOIN drops orphaned generations)\nconst res = await fetch(`/history/${id}`);\nif (res.status === 404) { removeFromList(id); return; }","typeGuard":"function isHistoryRow(r) {\n  return r != null && typeof r.id === 'string' && typeof r.profile_id === 'string';\n}","tryCatchPattern":"try {\n  const res = await fetch(`/history/${id}`);\n  if (res.status === 404) { removeFromList(id); return; }\n  const row = await res.json();\n} catch (e) { console.error(e); }","preventionTips":["Use a LEFT OUTER JOIN server-side so orphaned generations still resolve.","When deleting a profile, cascade or null profile_id on its generations.","Run consistency checks between generations and voice_profiles."],"tags":["fastapi","not-found","history","sqlalchemy","join","orphan","profile"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}