{"record":{"id":"36a578fe3349e707","repo":"jamiepine/voicebox","slug":"validation-error-from-validate-effects-chain","errorCode":null,"errorMessage":"{validation error from validate_effects_chain}","messagePattern":"\\{validation error from validate_effects_chain\\}","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"backend/routes/effects.py","lineNumber":38,"sourceCode":"    generation_id: str,\n    data: models.ApplyEffectsRequest,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Apply effects to a generation's clean audio and stream back without saving.\"\"\"\n    gen = db.query(DBGeneration).filter_by(id=generation_id).first()\n    if not gen:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n    if (gen.status or \"completed\") != \"completed\":\n        raise HTTPException(status_code=400, detail=\"Generation is not completed\")\n\n    from ..services import versions as versions_mod\n    from ..utils.effects import apply_effects, validate_effects_chain\n    from ..utils.audio import load_audio\n\n    chain_dicts = [e.model_dump() for e in data.effects_chain]\n    error = validate_effects_chain(chain_dicts)\n    if error:\n        raise HTTPException(status_code=400, detail=error)\n\n    all_versions = versions_mod.list_versions(generation_id, db)\n    clean_version = next((v for v in all_versions if v.effects_chain is None), None)\n    source_path = clean_version.audio_path if clean_version else gen.audio_path\n    resolved_source_path = config.resolve_storage_path(source_path)\n    if resolved_source_path is None or not resolved_source_path.exists():\n        raise HTTPException(status_code=404, detail=\"Source audio file not found\")\n\n    audio, sample_rate = await asyncio.to_thread(load_audio, str(resolved_source_path))\n    processed = await asyncio.to_thread(apply_effects, audio, sample_rate, chain_dicts)\n\n    import soundfile as sf\n\n    buf = io.BytesIO()\n    await asyncio.to_thread(lambda: sf.write(buf, processed, sample_rate, format=\"WAV\"))\n    buf.seek(0)\n\n    return StreamingResponse(","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/effects.py#L20-L56","documentation":"HTTP 400 raised by POST /effects/preview/{generation_id} when validate_effects_chain() returns a non-None error string. The validator checks: effects_chain is a list; each entry is a dict; the 'type' is in EFFECT_REGISTRY; 'params' is a dict; each param key is known; each param value is numeric and within [min, max]. The returned string identifies the first violation (e.g. unknown effect type, out-of-range param) and is passed verbatim as the 400 detail.","triggerScenarios":"Submitting an effects_chain with an unknown effect type, an unknown parameter name, a non-numeric parameter, or a numeric parameter outside its declared min/max range.","commonSituations":"Hand-building a chain with a typo'd effect name; using a param valid in an older version that was renamed/removed; sending string values for numeric params; exceeding a gain/frequency bound.","solutions":["Read the detail string: it names the effect index, the param, and the allowed range.","Cross-check effect types and param names against the EFFECT_REGISTRY (use the presets list or docs).","Clamp numeric params to the registry min/max before submitting.","Validate client-side with the same rules: list of dicts, known type, numeric in-range params."],"exampleFix":"// before\nconst chain = [{ type: 'reverb', params: { room_size: 5.0 } }]; // out of range\n// after\nconst chain = [{ type: 'reverb', params: { room_size: Math.min(1.0, Math.max(0.0, val)) } }];\nawait fetch(`/effects/preview/${genId}`, { method:'POST', body: JSON.stringify({ effects_chain: chain }) });","handlingStrategy":"validation","validationCode":"function validateChain(chain, registry) {\n  if (!Array.isArray(chain)) return 'must be a list';\n  for (let i = 0; i < chain.length; i++) {\n    const e = chain[i];\n    if (!registry[e.type]) return `unknown type at ${i}`;\n    for (const [k, v] of Object.entries(e.params || {})) {\n      const def = registry[e.type].params[k];\n      if (!def) return `unknown param ${k}`;\n      if (typeof v !== 'number') return `${k} must be number`;\n      if (v < def.min || v > def.max) return `${k} out of range`;\n    }\n  }\n  return null;\n}\nconst err = validateChain(chain, EFFECT_REGISTRY); if (err) throw new Error(err);","typeGuard":"function isValidEffect(e, registry): e is { type: string; params: Record<string, number> } {\n  return !!registry[e?.type];\n}","tryCatchPattern":"const r = await fetch(`/effects/preview/${id}`, { method:'POST', body: JSON.stringify(data) });\nif (r.status === 400) { const { detail } = await r.json(); /* validator message names effect index + param */ }","preventionTips":["Mirror validate_effects_chain rules on the client.","Clamp numeric params to registry min/max.","Use effect types and param names exactly as in EFFECT_REGISTRY."],"tags":["effects","http","validation","fastapi"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}