amir20/dozzle · error · Error

(await res.json().catch(() =>

Error message

(await res.json().catch(() => ({}))).error ?? res.statusText

What it means

DestinationCard.vue saves a notification destination via a POST/PUT fetch to the notifications API. When res.ok is false it throws an Error built from the backend JSON `error` field, falling back to res.statusText, which the catch block shows as a toast. The backend returns structured errors for invalid webhook URLs, unreachable destinations, or persistence failures.

Solutions

  1. Read the toast/JSON error field for the server-side reason and fix the destination url/template/headers accordingly
  2. Verify the destination URL is reachable and uses http/https
  3. Check ./data directory writability and backend logs if persist errors appear
  4. Re-authenticate if the status is 401/403, then resubmit the form
Defensive patterns

Strategy: validation

Validate before calling

new URL(destination.url); // throws on malformed url
if (destination.url && !/^https?:\/\//.test(destination.url)) {
  throw new Error("Destination url must use http or https");
}

Try / catch

try {
  await saveDestination(destination);
} catch (e) {
  showToast({ type: "error", message: e instanceof Error ? e.message : t("notifications.destination.copy-failed") });
}

Prevention

When it happens

Trigger: Saving a destination whose POST /api/notifications/destinations (or similar) returns non-2xx: malformed destination.url, invalid headers JSON, backend cannot persist to ./data/notifications.yml, or an expired session yields 401.

Common situations: Typo in webhook URL so the backend rejects validation; custom template with bad syntax rejected by server; read-only data directory causing persist failure; proxy returning 504 on slow destination test.

Related errors


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

Appendix: source

Thrown at assets/components/Notification/DestinationCard.vue:170

    const candidate = `${base} ${i}`;
    if (!taken.has(candidate)) return candidate;
  }
}

async function duplicateDestination() {
  try {
    const res = await fetch(withBase("/api/notifications/dispatchers"), {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        name: copyName(),
        type: destination.type,
        url: destination.url,
        template: destination.template,
        headers: destination.headers,
      }),
    });
    if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error ?? res.statusText);
    onUpdated?.();
  } catch (e) {
    showToast({ type: "error", message: e instanceof Error ? e.message : t("notifications.destination.copy-failed") });
  }
}

async function deleteDestination() {
  isDeleting.value = true;
  try {
    const res = await fetch(withBase(`/api/notifications/dispatchers/${destination.id}`), { method: "DELETE" });
    if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error ?? res.statusText);
    confirmingDelete.value = false;
    onUpdated?.();
  } catch (e) {
    showToast({
      type: "error",
      message: e instanceof Error ? e.message : t("notifications.destination.delete-failed"),
    });

View on GitHub (pinned to d9463cbe21)