{"record":{"id":"cb8080cb3a01d925","repo":"jamiepine/voicebox","slug":"invalid-source-source-must-be-one-of-sorted","errorCode":null,"errorMessage":"Invalid source '{source}'. Must be one of {sorted(VALID_SOURCES)}","messagePattern":"Invalid source '(.+?)'\\. Must be one of (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"warning","filePath":"backend/services/captures.py","lineNumber":71,"sourceCode":"        stt_model=row.stt_model,\n        llm_model=row.llm_model,\n        refinement_flags=flags_model,\n        created_at=row.created_at,\n    )\n\n\nasync def create_capture(\n    *,\n    audio_bytes: bytes,\n    filename: str,\n    source: str,\n    language: Optional[str],\n    stt_model: Optional[str],\n    db: Session,\n) -> CaptureResponse:\n    \"\"\"Persist raw audio, run STT, store the row.\"\"\"\n    if source not in VALID_SOURCES:\n        raise ValueError(f\"Invalid source '{source}'. Must be one of {sorted(VALID_SOURCES)}\")\n\n    capture_id = str(uuid.uuid4())\n    suffix = Path(filename).suffix.lower() or \".wav\"\n    if suffix not in (\".wav\", \".mp3\", \".m4a\", \".flac\", \".ogg\", \".webm\"):\n        suffix = \".wav\"\n\n    raw_path = config.get_captures_dir() / f\"{capture_id}{suffix}\"\n    written_files: list[Path] = []\n\n    try:\n        raw_path.write_bytes(audio_bytes)\n        written_files.append(raw_path)\n\n        # Decode once with librosa — its audioread fallback handles webm/opus\n        # via ffmpeg, which miniaudio (used inside mlx-audio's whisper) can't.\n        # The decoded array gives us an accurate duration and becomes the\n        # canonical WAV we hand to whisper.\n        try:","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/services/captures.py#L53-L89","documentation":"Raised as ValueError (translated to HTTP 400 by the /captures route) when create_capture is called with a source not in VALID_SOURCES = {'dictation','recording','file'}. It is a pure membership check at the top of the service, before any audio is written. The message lists the accepted set sorted.","triggerScenarios":"POST /captures with a `source` form field value outside the allowed set — e.g. 'upload', 'stream', 'voice', or an empty string. The route defaults source to 'file' when omitted, so this only fires when an explicit bad value is sent.","commonSituations":"Client sends its own source taxonomy that doesn't match the backend's; a new client build renamed the field values; integration code passing a literal typo.","solutions":["Send source as exactly one of 'dictation', 'recording', or 'file' (or omit it to default to 'file').","Centralize the allowed source list in a shared constant/enum on the client and sync it with VALID_SOURCES.","Add a client-side dropdown/enum so free-text can't produce an invalid value."],"exampleFix":"// before\nconst form = new FormData();\nform.set('source', 'upload');\n\n// after\nconst SOURCES = ['dictation', 'recording', 'file'] as const;\nform.set('source', SOURCES.includes(userSource) ? userSource : 'file');","handlingStrategy":"validation","validationCode":"const CAPTURE_SOURCES = ['dictation', 'recording', 'file'];\nfunction normalizeSource(s) {\n  return CAPTURE_SOURCES.includes(s) ? s : 'file';\n}\nform.set('source', normalizeSource(userSource));","typeGuard":"function isValidCaptureSource(s) {\n  return s === 'dictation' || s === 'recording' || s === 'file';\n}","tryCatchPattern":"try { await api.createCapture(file, source); }\ncatch (e) {\n  if (e.status === 400 && /Invalid source/.test(e.detail)) {\n    source = 'file'; await api.createCapture(file, source); // fall back to default\n  } else throw e;\n}","preventionTips":["Send source from a fixed enum shared with the backend's VALID_SOURCES.","Omit the field to accept the 'file' default rather than guessing.","Keep the client's source taxonomy in sync after backend changes."],"tags":["api","captures","validation","fastapi"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}