{"record":{"id":"ce2a3540b4145032","repo":"jamiepine/voicebox","slug":"error","errorCode":null,"errorMessage":"{error}","messagePattern":"\\{error\\}","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"backend/routes/profiles.py","lineNumber":354,"sourceCode":"\n@router.put(\"/profiles/{profile_id}/effects\", response_model=models.VoiceProfileResponse)\nasync def update_profile_effects(\n    profile_id: str,\n    data: models.ProfileEffectsUpdate,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Set or clear the default effects chain for a voice profile.\"\"\"\n    profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()\n    if not profile:\n        raise HTTPException(status_code=404, detail=\"Profile not found\")\n\n    if data.effects_chain is not None:\n        from ..utils.effects import validate_effects_chain\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        profile.effects_chain = _json.dumps(chain_dicts)\n    else:\n        profile.effects_chain = None\n\n    profile.updated_at = datetime.utcnow()\n    db.commit()\n    db.refresh(profile)\n\n    return _profile_to_response(profile)\n\n\n# ── Personality endpoint ──────────────────────────────────────────────\n# Only ``/profiles/{id}/compose`` remains — the UI's compose button\n# produces a fresh in-character utterance the user can edit before\n# speaking. Rewrite now happens inside ``/generate`` (and ``/speak``)\n# when ``personality=true``; there is no standalone rewrite/respond/speak\n# endpoint.\n","sourceCodeStart":336,"sourceCodeEnd":372,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/profiles.py#L336-L372","documentation":"Returned by PUT /profiles/{profile_id}/effects when validate_effects_chain() rejects the supplied chain (HTTP 400). The handler serializes each effect via model_dump, then delegates to validate_effects_chain; only if that returns a non-empty error string does it raise, using the validator's message verbatim. It means the request body parsed as ProfileEffectsUpdate but the chain contents are semantically invalid.","triggerScenarios":"Submitting an effects_chain with an unknown effect type, missing required parameters for an effect, out-of-range values, or a malformed effect object. Passing an empty object instead of a fully-formed effect descriptor.","commonSituations":"Frontend builds the chain from a stale list of supported effects after an engine upgrade that renamed/removed an effect. Copy-pasting a chain from docs for a different backend version. Negative or NaN gain values, unsupported sample-rate-dependent parameters.","solutions":["Inspect the exact detail string from the 400 response — it comes straight from validate_effects_chain and names the offending field/effect.","Cross-check the effect names and parameter keys against the current backend's supported effects list.","Validate the chain client-side against the same schema before submitting.","Ensure each effect object includes all required keys and that numeric params are within documented ranges."],"exampleFix":"// before\nawait api.put(`/profiles/${id}/effects`, { effects_chain: rawChain });\n\n// after\nconst errs = validateChainClient(rawChain); // mirror of validate_effects_chain\nif (errs.length) throw new UserError(errs);\nawait api.put(`/profiles/${id}/effects`, { effects_chain: rawChain });","handlingStrategy":"validation","validationCode":"function validateChainClient(chain) {\n  const supported = new Set(['eq','compressor','reverb','gain']);\n  const errs = [];\n  for (const e of chain) {\n    if (!supported.has(e.type)) errs.push(`unknown effect: ${e.type}`);\n    if (typeof e.params?.gain === 'number' && (e.params.gain < 0 || e.params.gain > 4))\n      errs.push(`gain out of range on ${e.type}`);\n  }\n  return errs;\n}","typeGuard":"function isEffectsChain(v) { return Array.isArray(v) && v.every(e => e && typeof e === 'object' && typeof e.type === 'string'); }","tryCatchPattern":"try { await api.put(`/profiles/${id}/effects`, { effects_chain: chain }); }\ncatch (e) { if (e.response?.status === 400) showUser(e.response.data.detail); throw e; }","preventionTips":["Mirror validate_effects_chain on the client and run it before submit.","Build the chain from the backend's current supported-effects list, not a hardcoded copy.","Surface the detail string verbatim to the user — it names the offending field."],"tags":["fastapi","http","validation","effects","profiles","rest"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}