gethomepage/homepage · error · Error

Unable to login to Duplicati (status ${status})

Error message

Unable to login to Duplicati (status ${status})

What it means

Thrown by the Duplicati proxy's login step when the POST to the login endpoint returns any status other than 200. Duplicati requires a login that exchanges the configured password for an AccessToken before any data request, so a failed login aborts the whole proxy call chain.

Source

Thrown at src/widgets/duplicati/proxy.js:62

    nextRun: nextRunTime?.toUTC().toISO() ?? null,
  };
}

async function login(widget) {
  const loginUrl = new URL(formatApiCall(widgets[widget.type].api, { endpoint: "auth/login", ...widget }));
  const [status, , data] = await httpProxy(loginUrl, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      Password: String(widget.password),
      RememberMe: true,
    }),
  });

  if (status !== 200) {
    throw new Error(`Unable to login to Duplicati (status ${status})`);
  }

  const body = asJson(data);
  if (!body?.AccessToken) {
    throw new Error("Duplicati login response did not include an access token");
  }

  return body.AccessToken;
}

async function apiGet(widget, endpoint, accessToken) {
  const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
  const [status, , data] = await httpProxy(url, {
    method: "GET",
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  });

View on GitHub (pinned to b6dca1ae03)

Solutions

  1. Re-enter the Duplicati password in the Homepage widget config exactly as set in Duplicati, trimming any whitespace.
  2. Confirm the Duplicati base URL and port are correct and reachable from the Homepage container.
  3. Log in to Duplicati's web UI with the same password to confirm the credential itself works.
  4. Check the reverse proxy passes the JSON POST body and Content-Type: application/json through unchanged.
  5. Raise Homepage debug logging to capture the status code that precedes the throw, then map it (401 vs 500 vs 502) to the right cause.

Example fix

// before
if (status !== 200) {
  throw new Error(`Unable to login to Duplicati (status ${status})`);
}
// after
if (status !== 200) {
  throw new Error(`Duplicati login at ${loginUrl} failed (HTTP ${status}): ${Buffer.from(data).toString().slice(0, 200)}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Sanity-check the Duplicati config before invoking login.
function validateDuplicatiWidget(w) {
  if (!w?.url || !/^https?:\/\//.test(w.url)) throw new Error("Duplicati url invalid");
  if (typeof w.password !== "string" || w.password.length === 0) throw new Error("Duplicati password missing");
  return true;
}

Type guard

function hasDuplicatiCredentials(widget) {
  return Boolean(widget && typeof widget.url === "string" && typeof widget.password === "string" && widget.password.length > 0);
}

Try / catch

try {
  const token = await login(widget);
  return await apiGet(widget, endpoint, token);
} catch (err) {
  if (/Unable to login/.test(err.message)) {
    res.status(401).json({ error: "Duplicati login failed", hint: "check password/URL" });
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: Wrong password configured for the Duplicati widget (login returns 401/422), Duplicati server unreachable or returning 500, login URL malformed so it 404s, or Duplicati restart/capacity lock returning 503 during the window the widget polls.

Common situations: Password copied with surrounding whitespace or quotes; Duplicati password changed but widget config not updated; base URL wrong (path/port/scheme); Duplicati version change that altered the login endpoint; running behind a reverse proxy that strips the POST body or sets the wrong Content-Type.

Related errors


AI-assisted analysis of gethomepage/homepage@b6dca1ae03 (2026-08-13). Data as JSON: /api/errors/d36b3a45cc21d8e9. Report an issue: GitHub.