amir20/dozzle · error · Error

Failed to save destination

Error message

Failed to save destination

What it means

WebhookDestinationForm.vue throws this when the POST that saves a webhook notification destination returns !res.ok. It prefers the server's JSON `error` field and falls back to the literal string 'Failed to save destination'.

Solutions

  1. Inspect the response status and body in devtools; prefer the server `error` message over the fallback string
  2. Verify the webhook URL is correct, reachable from the Dozzle host, and uses http/https
  3. Check server logs for the dispatcher creation failure and that the data directory is writable
  4. Ensure auth token/cookies are valid so the POST isn't rejected by middleware

Example fix

// before
if (!res.ok) {
  const data = await res.json();
  throw new Error(data.error || "Failed to save destination");
}
// after
if (!res.ok) {
  const data = await res.json().catch(() => ({}));
  throw new Error(data.error || `Failed to save destination (HTTP ${res.status})`);
}
Defensive patterns

Strategy: validation

Validate before calling

const url = new URL(input.url);
if (!/^https?:$/.test(url.protocol)) throw new Error("Webhook URL must be http/https");
if (!input.name?.trim()) throw new Error("Name is required");

Try / catch

try {
  const res = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input) });
  if (!res.ok) {
    const data = await res.json().catch(() => ({}));
    throw new Error(data.error || `Failed to save destination (HTTP ${res.status})`);
  }
} catch (e) {
  formError.value = e instanceof Error ? e.message : String(e);
}

Prevention

When it happens

Trigger: POST /api/notifications/dispatchers with webhook payload returns 4xx/5xx: invalid URL, unreachable test request on the server side, malformed body, or persistence failure writing the dispatchers file.

Common situations: Entering a webhook URL that the server cannot validate/reach (private IP, typo, https cert issues); notifications.yml is read-only; request rejected by reverse proxy with 401/404 HTML that isn't JSON.

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/9193013efa9a1288. Report an issue: GitHub.

Appendix: source

Thrown at assets/components/Notification/WebhookDestinationForm.vue:360

      type: "webhook",
      url: webhookUrl.value.trim(),
      template: template.value.trim() || undefined,
      headers: headersToRecord(),
    };

    const url = props.isEditing
      ? withBase(`/api/notifications/dispatchers/${props.destination!.id}`)
      : withBase("/api/notifications/dispatchers");

    const res = await fetch(url, {
      method: props.isEditing ? "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 destination");
    }

    registerGuard(null);
    // Hand the saved destination back so callers can select it right away.
    props.onCreated?.(await res.json().catch(() => undefined));
    props.close?.();
  } catch (e) {
    error.value = e instanceof Error ? e.message : "Failed to save destination";
  } finally {
    isSaving.value = false;
  }
}

// Unsaved-changes guard, matching the alert form: a stray Esc should not throw away a template.
const registerGuard = useDrawerCloseGuard();
const confirmingDiscard = ref(false);
const initial = ref<string>();
const snapshot = computed(() =>

View on GitHub (pinned to d9463cbe21)