jamiepine/voicebox · warning · HTTPException

Uploaded file is empty

Error message

Uploaded file is empty

What it means

Returned as a 400 from POST /captures when the uploaded audio file stream read zero bytes (the loop `while chunk := await file.read(UPLOAD_CHUNK_SIZE)` produced an empty concatenation). The route streams the upload into memory in chunks and explicitly rejects empty bodies before invoking STT, because a zero-length audio would fail downstream anyway. This is a client-side input error.

Source

Thrown at backend/routes/captures.py:39

UPLOAD_CHUNK_SIZE = 1024 * 1024  # 1 MB


@router.post("/captures", response_model=models.CaptureCreateResponse)
async def create_capture_endpoint(
    file: UploadFile = File(...),
    source: str = Form("file"),
    language: str | None = Form(None),
    stt_model: str | None = Form(None),
    db: Session = Depends(get_db),
):
    """Upload audio, run STT, persist the capture."""
    chunks = []
    while chunk := await file.read(UPLOAD_CHUNK_SIZE):
        chunks.append(chunk)
    audio_bytes = b"".join(chunks)

    if not audio_bytes:
        raise HTTPException(status_code=400, detail="Uploaded file is empty")

    saved = settings_service.get_capture_settings(db)
    resolved_stt = stt_model or saved.stt_model
    if language is None:
        resolved_language = None if saved.language == "auto" else saved.language
    else:
        resolved_language = None if language == "auto" else language

    try:
        capture = await captures_service.create_capture(
            audio_bytes=audio_bytes,
            filename=file.filename or "capture.wav",
            source=source,
            language=resolved_language,
            stt_model=resolved_stt,
            db=db,
        )
    except ValueError as e:

View on GitHub (pinned to 51f49dea19)

Solutions

  1. On the client, check the recorded Blob's size > 0 before appending to FormData and posting.
  2. Verify microphone permissions are granted so the recorder actually captures samples.
  3. Ensure the recorder's stop() is only called after audio data has arrived (min-duration guard).
  4. Log file.size and file.type on the client before upload to catch empty captures early.

Example fix

// before
const fd = new FormData();
fd.append('file', blob);
await fetch('/captures', { method: 'POST', body: fd });
// after
if (!blob || blob.size === 0) {
  toast('No audio captured');
  return;
}
const fd = new FormData();
fd.append('file', blob);
await fetch('/captures', { method: 'POST', body: fd });
Defensive patterns

Strategy: validation

Validate before calling

// Client-side guard before upload.
if (!blob || blob.size === 0) {
  throw new Error('No audio recorded; refusing to upload empty file');
}
const fd = new FormData();
fd.append('file', blob, 'capture.wav');

Type guard

function isNonEmptyAudio(blob: Blob | null | undefined): blob is Blob {
  return blob instanceof Blob && blob.size > 0;
}

Try / catch

try {
  const r = await fetch('/captures', { method: 'POST', body: fd });
  if (r.status === 400) {
    const body = await r.json();
    if (body.detail === 'Uploaded file is empty') {
      toast('Recording was empty; check mic access');
      return;
    }
    throw new Error(body.detail);
  }
} catch (e) { showNetworkError(e); }

Prevention

When it happens

Trigger: POST /captures with a multipart 'file' field that is empty, or a filename pointing to a 0-byte file, or a request where the file part was sent without a body (e.g. frontend sent FormData with an empty Blob).

Common situations: Recorder started and stopped with no audio captured (mic permission denied silently, or instant stop); the OS produced a 0-byte temp file; a bug in the client where an empty Blob is appended to FormData.

Related errors


AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12). Data as JSON: /api/errors/15582537e4545325. Report an issue: GitHub.