{"record":{"id":"097d37c5f8ae9845","repo":"dani-garcia/vaultwarden","slug":"unable-to-update-emergency-access-notification-dat","errorCode":null,"errorMessage":"Unable to update emergency access notification date","messagePattern":"Unable to update emergency access notification date","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/api/core/emergency_access.rs","lineNumber":807,"sourceCode":"\n        let now = Utc::now().naive_utc();\n        for mut emer in emergency_access_list {\n            // The find_all_recoveries_initiated already checks if the recovery_initiated_at is not null (None)\n            // Calculate the day before the recovery will become active\n            let final_recovery_reminder_at =\n                emer.recovery_initiated_at.unwrap() + TimeDelta::try_days(i64::from(emer.wait_time_days - 1)).unwrap();\n            // Calculate if a day has passed since the previous notification, else no notification has been sent before\n            let next_recovery_reminder_at = if let Some(last_notification_at) = emer.last_notification_at {\n                last_notification_at + TimeDelta::try_days(1).unwrap()\n            } else {\n                now\n            };\n            if final_recovery_reminder_at.le(&now) && next_recovery_reminder_at.le(&now) {\n                // Only update the last notification date\n                // Updating the whole record could cause issues when the emergency_request_timeout_job is also active\n                emer.update_last_notification_date_and_save(&now, &conn)\n                    .await\n                    .expect(\"Unable to update emergency access notification date\");\n\n                if CONFIG.mail_enabled() {\n                    // get grantor user to send Accepted email\n                    let grantor_user =\n                        User::find_by_uuid(&emer.grantor_uuid, &conn).await.expect(\"Grantor user not found\");\n\n                    // get grantee user to send Accepted email\n                    let grantee_user =\n                        User::find_by_uuid(&emer.grantee_uuid.clone().expect(\"Grantee user invalid\"), &conn)\n                            .await\n                            .expect(\"Grantee user not found\");\n\n                    mail::send_emergency_access_recovery_reminder(\n                        &grantor_user.email,\n                        &grantee_user.name,\n                        emer.get_type_as_str(),\n                        \"1\", // This notification is only triggered one day before the activation\n                    )","sourceCodeStart":789,"sourceCodeEnd":825,"githubUrl":"https://github.com/dani-garcia/vaultwarden/blob/0cefa4cca7c9f2a5579dd290f78193b543818c51/src/api/core/emergency_access.rs#L789-L825","documentation":"emergency_notification_reminder_job: one day before a recovery activates, it records the notification via update_last_notification_date_and_save(&now, &conn).expect(\"Unable to update emergency access notification date\"). A DB failure panics the reminder job task. The code deliberately updates a narrow column to avoid races with the timeout job, so the realistic cause is DB unavailability rather than row conflicts.","triggerScenarios":"The reminder job firing during DB unavailability or lock contention while updating last_notification_at for a due recovery.","commonSituations":"SQLite lock contention; DB failover at job time; disk full — the same conditions that break the other job writes.","solutions":["Restore DB health and restart the service to respawn the job","Reduce write contention (WAL for SQLite, fewer concurrent jobs)","Verify last_notification_at afterwards to avoid duplicate reminder mails on rerun","Code fix: log-and-continue per row instead of expect"],"exampleFix":"// before\nemer.update_last_notification_date_and_save(&now, &conn).await.expect(\"Unable to update emergency access notification date\");\n// after\nif let Err(e) = emer.update_last_notification_date_and_save(&now, &conn).await {\n    error!(\"Failed to record reminder for emergency access {}: {e}\", emer.uuid);\n    continue;\n}","handlingStrategy":"try-catch","validationCode":"-- Rows the reminder job will process; confirm they look healthy first\nSELECT uuid, grantor_uuid, grantee_uuid, wait_time_days, recovery_initiated_at, last_notification_at\nFROM emergency_access\nWHERE recovery_initiated_at IS NOT NULL;","typeGuard":null,"tryCatchPattern":"if let Err(e) = emer.update_last_notification_date_and_save(&now, &conn).await {\n    error!(\"reminder job: failed to record notification for {}: {e}\", emer.uuid);\n    continue;\n}","preventionTips":["Keep the DB healthy at reminder intervals","After incidents, check last_notification_at to avoid duplicate reminders","Prefer narrow-column updates (as the code does) over whole-row saves to avoid job races"],"tags":["rust","emergency-access","database","panic","background-jobs"],"backgroundTag":null,"analyzedSha":"0cefa4cca7c9f2a5579dd290f78193b543818c51","analyzedAt":"2026-08-16T07:44:56.102Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}