{"record":{"id":"4445a7f39c57db01","repo":"jamiepine/voicebox","slug":"str-e-4445a7","errorCode":null,"errorMessage":"str(e)","messagePattern":"str\\(e\\)","errorType":"http","errorClass":"HTTPException","httpStatus":500,"severity":"error","filePath":"backend/routes/stories.py","lineNumber":237,"sourceCode":"\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\n        safe_name = \"\".join(c for c in story.name if c.isalnum() or c in (\" \", \"-\", \"_\")).strip()\n        if not safe_name:\n            safe_name = \"story\"\n        filename = f\"{safe_name}.wav\"\n\n        return StreamingResponse(\n            io.BytesIO(audio_bytes),\n            media_type=\"audio/wav\",\n            headers={\"Content-Disposition\": safe_content_disposition(\"attachment\", filename)},\n        )\n    except HTTPException:\n        raise\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=str(e))\n","sourceCodeStart":219,"sourceCodeEnd":238,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/stories.py#L219-L238","documentation":"Raised (HTTP 500) by GET /stories/{story_id}/export-audio as the catch-all for any non-HTTPException thrown during export (the handler re-raises genuine HTTPException, then wraps everything else). The detail is the raw str(e), so the client sees whatever exception message the audio-mixing stack produced. Common underlying causes live in stories.export_story_audio: librosa/soundfile decode failures, missing generation audio files on disk, numpy shape mismatches, or sample-rate assumptions (it defaults to 24000 Hz).","triggerScenarios":"A story item's generation audio file is missing from disk (deleted/moved data dir), an audio decode error in the mixing loop, or a malformed/zero-length audio array causing the concatenation/mixing math to throw.","commonSituations":"Data directory moved or partially restored from backup; a generation row points at a path that no longer exists; mismatched sample rates between generations breaking the mix; disk full during temp WAV write.","solutions":["Check the server logs for the wrapped exception's traceback — the real cause is there, not in the 500 detail.","Verify every item's generation has an on-disk audio file before exporting.","Replace detail=str(e) with a generic message and log the exception server-side to avoid leaking internals.","If sample-rate mismatches are the cause, ensure all generations are produced at the same rate (the service hardcodes 24000)."],"exampleFix":"# before\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n# after\n    except Exception:\n        logger.exception(\"export_story_audio failed for story %s\", story_id)\n        raise HTTPException(status_code=500, detail=\"Audio export failed; see server logs\")","handlingStrategy":"try-catch","validationCode":"// Pre-flight: ensure every item has a resolvable audio asset before export.\nasync function preflightExport(api, storyId) {\n  const story = await api.getStory(storyId).then(r => r.json());\n  const missing = (story.items ?? []).filter(i => !i.audio_path);\n  if (missing.length) throw new Error(`${missing.length} item(s) missing audio`);\n  return story;\n}","typeGuard":"function itemsHaveAudio(story) {\n  return (story.items ?? []).every(i => typeof i.audio_path === 'string' && i.generation_id);\n}","tryCatchPattern":"try { await api.exportStoryAudio(storyId); }\ncatch (e) {\n  if (e.status === 500) {\n    notify('Export failed; the server log has details. Check for missing audio files.');\n    // do NOT blind-retry; surface to the user\n  } else throw e;\n}","preventionTips":["Don't retry a 500 blindly — read the server traceback first.","Keep generation audio files co-located with the data dir; never delete them while rows reference them.","Avoid detail=str(e) server-side; log internally and return a generic message."],"tags":["api","stories","fastapi","error-handling","information-disclosure","audio"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}