{"record":{"id":"c010b521044de1a6","repo":"jamiepine/voicebox","slug":"unsupported-audio-format-suffix-allowed-sor","errorCode":null,"errorMessage":"Unsupported audio format '{suffix}'. Allowed: {sorted(IMPORT_AUDIO_EXTENSIONS)}","messagePattern":"Unsupported audio format '(.+?)'\\. Allowed: (.+?)","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"backend/routes/generations.py","lineNumber":429,"sourceCode":"        _wav_stream(),\n        media_type=\"audio/wav\",\n        headers={\"Content-Disposition\": 'attachment; filename=\"speech.wav\"'},\n    )\n\n\n@router.post(\"/generate/import\", response_model=models.GenerationResponse)\nasync def import_audio(\n    file: UploadFile = File(...),\n    db: Session = Depends(get_db),\n):\n    \"\"\"Register an external audio file as a generation row.\n\n    Designed for the story timeline so users can drop in music or other\n    non-TTS audio. The row points at a singleton \"Imported Audio\" profile\n    so the existing generation/story plumbing keeps working unchanged.\"\"\"\n    suffix = Path(file.filename or \"\").suffix.lower()\n    if suffix not in IMPORT_AUDIO_EXTENSIONS:\n        raise HTTPException(\n            status_code=400,\n            detail=f\"Unsupported audio format '{suffix}'. Allowed: {sorted(IMPORT_AUDIO_EXTENSIONS)}\",\n        )\n\n    chunks: list[bytes] = []\n    total = 0\n    while True:\n        chunk = await file.read(1024 * 1024)\n        if not chunk:\n            break\n        total += len(chunk)\n        if total > IMPORT_AUDIO_MAX_BYTES:\n            raise HTTPException(\n                status_code=413,\n                detail=f\"File exceeds {IMPORT_AUDIO_MAX_BYTES // (1024 * 1024)} MB limit.\",\n            )\n        chunks.append(chunk)\n    audio_bytes = b\"\".join(chunks)","sourceCodeStart":411,"sourceCodeEnd":447,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/generations.py#L411-L447","documentation":"Returned by POST /generate/import when the uploaded file's extension is not in IMPORT_AUDIO_EXTENSIONS = {'.wav','.mp3','.flac','.ogg','.m4a','.aac','.webm'}. The check is purely on the filename suffix via `Path(file.filename or '').suffix.lower()`. HTTP 400. The response detail lists the allowed set sorted, so the client can present it.","triggerScenarios":"Uploading a file with an extension outside the allow-list (e.g. .opus, .aiff, .mp4, .txt); uploading a file with no extension (filename == '' or no dot); uploading with an uppercase extension that isn't lowered (note: suffix IS lowercased, so this is fine, but a truly unsupported extension still fails).","commonSituations":"User drags a .mp4 video or .opus clip into the story timeline import; macOS hidden-file upload with no extension; a renamed file whose new extension isn't whitelisted.","solutions":["Convert the source to one of the allowed formats before uploading (wav/mp3/flac/ogg/m4a/aac/webm).","If a common format like .opus is needed, add it to IMPORT_AUDIO_EXTENSIONS in backend/routes/generations.py and ensure load_audio (the decoder downstream) supports it.","Validate the filename extension on the client before the upload to give immediate feedback.","Handle the 400 response by surfacing the returned allowed-formats list to the user."],"exampleFix":"// before\n<input type=\"file\" />\n\n// after\nconst ALLOWED = ['.wav','.mp3','.flac','.ogg','.m4a','.aac','.webm'];\n<input type=\"file\" accept={ALLOWED.join(',')} onChange={e => {\n  if (!ALLOWED.some(ext => e.target.files[0].name.toLowerCase().endsWith(ext))) {\n    alert('Unsupported format'); return;\n  }\n  upload(e.target.files[0]);\n}} />","handlingStrategy":"validation","validationCode":"const ALLOWED = ['.wav','.mp3','.flac','.ogg','.m4a','.aac','.webm'];\nfunction allowedExt(name) {\n  return ALLOWED.includes(name.slice(name.lastIndexOf('.')).toLowerCase());\n}\nif (!allowedExt(file.name)) { alert('Unsupported format'); return; }","typeGuard":"const IMPORT_AUDIO_EXTENSIONS = ['.wav','.mp3','.flac','.ogg','.m4a','.aac','.webm'];\nfunction hasImportableAudioExt(filename) {\n  const dot = filename.lastIndexOf('.');\n  return dot >= 0 && IMPORT_AUDIO_EXTENSIONS.includes(filename.slice(dot).toLowerCase());\n}","tryCatchPattern":"try {\n  const res = await fetch('/generate/import', { method:'POST', body: form });\n  if (res.status === 400) {\n    const detail = await res.json();\n    showFormatError(detail.detail); // includes allowed list\n    return;\n  }\n} catch (e) { console.error(e); }","preventionTips":["Set the file input `accept` attribute to the allowed list.","Validate the suffix client-side before uploading.","Keep the client allow-list in sync with IMPORT_AUDIO_EXTENSIONS."],"tags":["fastapi","file-upload","audio","import","validation","format"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}