amir20/dozzle · error · Error

Failed to save alert

Error message

Failed to save alert

What it means

saveAlert() POSTs (or PUTs when editing) an alert rule to the notifications API. When the server responds with a non-OK status, it reads the JSON error field from the response body and throws it; the literal string "Failed to save alert" is the fallback used when the response body has no error field or the body is not parseable. The caught message is stored in saveError for display next to the form.

Solutions

  1. Read saveError.value (or the thrown message) for the server-provided error string and fix the offending field in the form payload.
  2. Verify the alert payload matches the backend alert schema: required name, at least one pattern/rule, valid channel destinations.
  3. Check the backend logs for the corresponding request to /api/notifications (or alerts) to see the 4xx/5xx cause, e.g. unwritable notifications.yml.
  4. If editing, confirm the alert still exists server-side; if it was removed, create a new one instead of PUTting to a stale id.

Example fix

// before
await saveAlert(input); // throws generic "Failed to save alert"
// after
if (!input.name?.trim()) {
  saveError.value = "Alert name is required";
  return;
}
try {
  await saveAlert(input);
} catch (e) {
  console.error("alert save rejected by server", e);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// before calling saveAlert
function validateAlertInput(input: AlertInput): string | null {
  if (!input.name?.trim()) return "Alert name is required";
  if (!input.patterns?.length) return "At least one pattern is required";
  return null;
}

Try / catch

try {
  await saveAlert(input);
} catch (e) {
  saveError.value = e instanceof Error ? e.message : "Unexpected error saving alert";
}

Prevention

When it happens

Trigger: Calling saveAlert() while the backend returns res.ok === false, e.g. the alert payload fails server-side validation, an alert with the same name already exists (conflict on POST), the alert being edited no longer exists (404 on PUT), or the session is unauthenticated (401).

Common situations: Submitting the alert form with fields the backend rejects (empty name, invalid schedule, bad regex); the data/notifications.yml file is unwritable so the backend persist step fails (500); editing an alert that was deleted in another tab (404); auth configured and the token expired (401).

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of amir20/dozzle@d9463cbe21 (2026-09-07). Data as JSON: /api/errors/6cc92b42c1c5a208. Report an issue: GitHub.

Appendix: source

Thrown at assets/composable/alertForm.ts:103

    try {
      const input = {
        name: alertName.value.trim(),
        containerExpression: containerExpression.value,
        dispatcherId: dispatcherId.value,
        enabled: options.alert?.enabled ?? true,
        ...typeSpecificFields,
      };
      const url = isEditing.value
        ? withBase(`/api/notifications/rules/${options.alert!.id}`)
        : withBase("/api/notifications/rules");
      const res = await fetch(url, {
        method: isEditing.value ? "PUT" : "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(input),
      });
      if (!res.ok) {
        const data = await res.json();
        throw new Error(data.error || "Failed to save alert");
      }
      options.onCreated?.();
      options.close?.();
    } catch (e) {
      saveError.value = e instanceof Error ? e.message : "Failed to save alert";
    } finally {
      isSaving.value = false;
    }
  }

  async function validatePreview(extraFields: Record<string, unknown> = {}) {
    if (!containerExpression.value && !Object.values(extraFields).some(Boolean)) {
      containerResult.value = null;
      return { data: null };
    }

    isLoading.value = true;
    try {

View on GitHub (pinned to d9463cbe21)