mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-15 21:00:42 -07:00
fix: handle non-ASCII filenames in Content-Disposition headers
The export endpoints (export-audio, export generation, export profile, export story) crash with `'latin-1' codec can't encode characters` when the generated text or profile/story name contains non-ASCII characters (e.g. Cyrillic, Chinese, Arabic). Root cause: Python's `str.isalnum()` passes Unicode letters through to the filename, but HTTP headers are encoded as latin-1 by the ASGI server, which cannot represent characters outside the 0-255 range. Fix: introduce `_safe_content_disposition()` helper that builds a standards-compliant header with an ASCII-only `filename` fallback and a RFC 5987 `filename*=UTF-8''...` parameter for Unicode-capable clients. Fixes #68 Co-authored-by: Cursor <[email protected]>
This commit is contained in:
+22
-4
@@ -22,6 +22,24 @@ import uuid
|
||||
import asyncio
|
||||
import signal
|
||||
import os
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
def _safe_content_disposition(disposition_type: str, filename: str) -> str:
|
||||
"""Build a Content-Disposition header that is safe for non-ASCII filenames.
|
||||
|
||||
Uses RFC 5987 ``filename*`` parameter so that browsers can decode
|
||||
UTF-8 filenames while the ``filename`` fallback stays ASCII-only.
|
||||
"""
|
||||
ascii_name = "".join(
|
||||
c for c in filename if c.isascii() and (c.isalnum() or c in " -_.")
|
||||
).strip() or "download"
|
||||
utf8_name = quote(filename, safe="")
|
||||
return (
|
||||
f'{disposition_type}; filename="{ascii_name}"; '
|
||||
f"filename*=UTF-8''{utf8_name}"
|
||||
)
|
||||
|
||||
|
||||
from . import database, models, profiles, history, tts, transcribe, config, export_import, channels, stories, __version__
|
||||
from .database import get_db, Generation as DBGeneration, VoiceProfile as DBVoiceProfile
|
||||
@@ -388,7 +406,7 @@ async def export_profile(
|
||||
io.BytesIO(zip_bytes),
|
||||
media_type="application/zip",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"'
|
||||
"Content-Disposition": _safe_content_disposition("attachment", filename)
|
||||
}
|
||||
)
|
||||
except ValueError as e:
|
||||
@@ -753,7 +771,7 @@ async def export_generation(
|
||||
io.BytesIO(zip_bytes),
|
||||
media_type="application/zip",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"'
|
||||
"Content-Disposition": _safe_content_disposition("attachment", filename)
|
||||
}
|
||||
)
|
||||
except ValueError as e:
|
||||
@@ -786,7 +804,7 @@ async def export_generation_audio(
|
||||
audio_path,
|
||||
media_type="audio/wav",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"'
|
||||
"Content-Disposition": _safe_content_disposition("attachment", filename)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1054,7 +1072,7 @@ async def export_story_audio(
|
||||
io.BytesIO(audio_bytes),
|
||||
media_type="audio/wav",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"'
|
||||
"Content-Disposition": _safe_content_disposition("attachment", filename)
|
||||
}
|
||||
)
|
||||
except HTTPException:
|
||||
|
||||
Reference in New Issue
Block a user