{"record":{"id":"752e522692af6b93","repo":"dani-garcia/vaultwarden","slug":"error-saving-attachment","errorCode":null,"errorMessage":"Error saving attachment","messagePattern":"Error saving attachment","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/api/core/ciphers.rs","lineNumber":1158,"sourceCode":") -> JsonResult {\n    let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {\n        err!(\"Cipher doesn't exist\")\n    };\n\n    if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn).await {\n        err!(\"Cipher is not write accessible\")\n    }\n\n    let data: AttachmentRequestData = data.into_inner();\n    let file_size = data.file_size.into_i64()?;\n\n    if file_size < 0 {\n        err!(\"Attachment size can't be negative\")\n    }\n    let attachment_id = crypto::generate_attachment_id();\n    let attachment =\n        Attachment::new(attachment_id.clone(), cipher.uuid.clone(), data.file_name, file_size, Some(data.key));\n    attachment.save(&conn).await.expect(\"Error saving attachment\");\n\n    let url = format!(\"/ciphers/{}/attachment/{attachment_id}\", cipher.uuid);\n    let response_key = match data.admin_request {\n        Some(b) if b => \"cipherMiniResponse\",\n        _ => \"cipherResponse\",\n    };\n\n    Ok(Json(json!({ // AttachmentUploadDataResponseModel\n        \"object\": \"attachment-fileUpload\",\n        \"attachmentId\": attachment_id,\n        \"url\": url,\n        \"fileUploadType\": FileUploadType::Direct as i32,\n        response_key: cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &conn).await?,\n    })))\n}\n\n#[derive(FromForm)]\nstruct UploadData<'f> {","sourceCodeStart":1140,"sourceCodeEnd":1176,"githubUrl":"https://github.com/dani-garcia/vaultwarden/blob/0cefa4cca7c9f2a5579dd290f78193b543818c51/src/api/core/ciphers.rs#L1140-L1176","documentation":"Start of the v2 attachment flow (POST /ciphers/{uuid}/attachment/v2): after write-access checks and file_size parsing, a new Attachment row is inserted with attachment.save(&conn).await.expect(\"Error saving attachment\") — any DB failure panics the request before file bytes are transferred, so the client can simply retry once the DB is healthy.","triggerScenarios":"Starting an attachment upload from the web vault/CLI when the attachment insert fails: locked SQLite, dead connection, constraint violation, or full disk.","commonSituations":"Concurrent uploads on SQLite without WAL; DB failover mid-session; migrations not applied after an upgrade; batch imports creating many attachments at once.","solutions":["Restore DB health and retry the upload","Enable WAL / reduce concurrent writers for SQLite","Apply migrations and verify the attachments table schema","Code fix: propagate the save error instead of expect"],"exampleFix":"// before\nattachment.save(&conn).await.expect(\"Error saving attachment\");\n// after\nattachment.save(&conn).await.map_err(|e| Error::new(\"Failed to save attachment\", e.to_string()))?;","handlingStrategy":"try-catch","validationCode":"# Confirm the attachments table is reachable before mass uploads\nsqlite3 /data/db.sqlite3 \"SELECT count(*) FROM attachments;\" && echo attachments-table-ok","typeGuard":null,"tryCatchPattern":"match attachment.save(&conn).await {\n    Ok(_) => {}\n    Err(e) => {\n        error!(\"Failed to save attachment {}: {e}\", attachment.id);\n        return Err(Error::new(\"Failed to save attachment\", e.to_string()));\n    }\n}","preventionTips":["Keep DB writes non-contended during bulk uploads","Back up before mass upload operations","Watch logs for repeated save failures indicating systemic DB problems"],"tags":["rust","database","attachments","panic","ciphers"],"backgroundTag":null,"analyzedSha":"0cefa4cca7c9f2a5579dd290f78193b543818c51","analyzedAt":"2026-08-16T07:44:56.102Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}