{"record":{"id":"57cdfcbe452f6746","repo":"unslothai/unsloth","slug":"str-exc-57cdfc","errorCode":null,"errorMessage":"str(exc)","messagePattern":"str\\(exc\\)","errorType":"http","errorClass":"HTTPException","httpStatus":409,"severity":"warning","filePath":"studio/backend/routes/settings.py","lineNumber":430,"sourceCode":"        lambda stored, submitted: _preserve_recovered_defaults(\n            VideoGenerationPresetState, stored, submitted\n        ),\n    )\n    return {\"saved\": True}\n\n\ndef _upsert_custom_generation_preset(\n    kind: Literal[\"image\", \"video\"], payload: ImageGenerationPreset | VideoGenerationPreset\n) -> dict[str, bool]:\n    try:\n        schema = type(payload)\n        upsert_media_generation_preset(\n            kind,\n            payload.model_dump(),\n            lambda stored: _validated_readable_model(schema, stored) is not None,\n        )\n    except ValueError as exc:\n        raise HTTPException(status_code = 409, detail = str(exc)) from exc\n    return {\"saved\": True}\n\n\n@router.put(\"/generation-presets/image/custom\")\ndef upsert_custom_image_generation_preset(\n    payload: ImageGenerationPreset, current_subject: str = Depends(get_current_subject)\n) -> dict[str, bool]:\n    return _upsert_custom_generation_preset(\"image\", payload)\n\n\n@router.put(\"/generation-presets/video/custom\")\ndef upsert_custom_video_generation_preset(\n    payload: VideoGenerationPreset, current_subject: str = Depends(get_current_subject)\n) -> dict[str, bool]:\n    return _upsert_custom_generation_preset(\"video\", payload)\n\n\n@router.delete(\"/generation-presets/{kind}/custom\")","sourceCodeStart":412,"sourceCodeEnd":448,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/routes/settings.py#L412-L448","documentation":"HTTP 409 from PUT /settings/generation-presets/{image|video}/custom. The route calls upsert_media_generation_preset and converts any ValueError into 409 with str(exc). The stored-preset store raises ValueError('Delete a preset before saving another one') when you add a NEW preset name while the readable-preset slots are already at the maximum count (_MAX_PRESETS). The lambda also re-validates that each stored preset round-trips through the current pydantic schema; unrelated schema failures surface as other errors.","triggerScenarios":"PUT a custom image or video generation preset with a name that does not already exist while the kind already stores the maximum number of readable custom presets; hitting the cap after recovering/migrating presets; repeatedly saving presets without deleting.","commonSituations":"Users accumulating generation presets up to the cap; teams importing preset collections; UI not surfacing the remaining-preset count.","solutions":["DELETE /settings/generation-presets/{kind}/custom?name=<old> for a preset you no longer need, then retry the PUT.","Overwrite an existing preset instead of adding a new one — same name replaces in place and never hits the cap.","Show the preset count/limit in the UI and disable 'Save as new' when full."],"exampleFix":"// before\nawait api.put('/settings/generation-presets/image/custom', { name: `Style ${n+1}`, params }); // 409 cap\n\n// after\nif (presets.length >= MAX_PRESETS) {\n  await api.delete(`/settings/generation-presets/image/custom?name=${encodeURIComponent(oldest)}`);\n}\nawait api.put('/settings/generation-presets/image/custom', { name: `Style ${n+1}`, params });","handlingStrategy":"validation","validationCode":"const existing = await api.getCustomPresets(kind);\nconst isReplace = existing.some(p => p.name === payload.name);\nif (!isReplace && existing.length >= MAX_PRESETS) {\n  throw new Error('Preset limit reached — delete one first');\n}\nawait api.put(`/settings/generation-presets/${kind}/custom`, payload);","typeGuard":"function canSavePreset(list: { name: string }[], name: string, max: number): boolean {\n  return list.some(p => p.name === name) || list.length < max;\n}","tryCatchPattern":"try { await api.put(presetUrl, payload); }\ncatch (e) {\n  if (e.status === 409 && /Delete a preset/.test(e.detail)) { promptDeleteOldest(); return; }\n  throw e;\n}","preventionTips":["Track the preset count and cap in the client UI.","Prefer overwriting an existing name over creating near-duplicates.","Offer delete-from-list inline so the cap never surprises the user."],"tags":["fastapi","http-409","presets","quota","validation"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}