{"record":{"id":"65782ecdeafa8fda","repo":"gethomepage/homepage","slug":"duplicati-login-response-did-not-include-an-access","errorCode":null,"errorMessage":"Duplicati login response did not include an access token","messagePattern":"Duplicati login response did not include an access token","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/widgets/duplicati/proxy.js","lineNumber":67,"sourceCode":"  const loginUrl = new URL(formatApiCall(widgets[widget.type].api, { endpoint: \"auth/login\", ...widget }));\n  const [status, , data] = await httpProxy(loginUrl, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      Password: String(widget.password),\n      RememberMe: true,\n    }),\n  });\n\n  if (status !== 200) {\n    throw new Error(`Unable to login to Duplicati (status ${status})`);\n  }\n\n  const body = asJson(data);\n  if (!body?.AccessToken) {\n    throw new Error(\"Duplicati login response did not include an access token\");\n  }\n\n  return body.AccessToken;\n}\n\nasync function apiGet(widget, endpoint, accessToken) {\n  const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));\n  const [status, , data] = await httpProxy(url, {\n    method: \"GET\",\n    headers: {\n      Authorization: `Bearer ${accessToken}`,\n    },\n  });\n\n  if (status !== 200) {\n    throw new Error(`Duplicati request failed for ${endpoint}`);\n  }\n","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/gethomepage/homepage/blob/b6dca1ae033e613d8e692f9a161a3cc53a5a2857/src/widgets/duplicati/proxy.js#L49-L85","documentation":"Thrown after a successful (200) Duplicati login when the response body parses as JSON but does not contain an AccessToken field. The HTTP layer succeeded but the contract is broken: Duplicati's login endpoint is expected to return { AccessToken: \"...\" }, and without it the subsequent API calls have no bearer token.","triggerScenarios":"Duplicati returns 200 with an unexpected body shape (an HTML page from a reverse proxy, an empty object, an error envelope), an API version change renamed the field, or the login endpoint was redirected to a UI page that still returned 200.","commonSituations":"Reverse proxy (e.g. a custom 200 error/landing page) intercepting the login path; Duplicati upgrade that changed the auth response schema; base URL pointing at the web UI root instead of the API; a captive/redirecting network returning 200 HTML instead of the API JSON.","solutions":["Verify the widget's URL targets the Duplicati API base, not the web UI root.","Reproduce the login POST with curl from the Homepage host and inspect the response JSON to confirm AccessToken is present.","Check the Duplicati version against the one Homepage's widget was written for; update Homepage or align versions.","Ensure no reverse proxy rewrites the login path or serves a static page on it.","Temporarily log asJson(data) to see exactly what Duplicati returned and adjust the parsing expectation."],"exampleFix":"// before\nif (!body?.AccessToken) {\n  throw new Error(\"Duplicati login response did not include an access token\");\n}\n// after\nif (!body?.AccessToken) {\n  throw new Error(`Duplicati login response missing AccessToken; got: ${JSON.stringify(body).slice(0, 200)}`);\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function isDuplicatiLoginBody(v) {\n  return typeof v === \"object\" && v !== null && typeof v.AccessToken === \"string\" && v.AccessToken.length > 0;\n}","tryCatchPattern":"const body = asJson(data);\nif (!isDuplicatiLoginBody(body)) {\n  // Surface what Duplicati actually returned so the misconfigured proxy is obvious.\n  throw new Error(`Unexpected Duplicati login body: ${JSON.stringify(body).slice(0, 200)}`);\n}","preventionTips":["Point the widget at the Duplicati API base, not the UI root, so the login endpoint returns JSON, not HTML.","Pin the Duplicati version in test fixtures so a schema change is caught before production.","Assert the AccessToken shape in an integration test against a real Duplicati instance."],"tags":["duplicati","authentication","api-contract","widget","response-validation"],"backgroundTag":null,"analyzedSha":"b6dca1ae033e613d8e692f9a161a3cc53a5a2857","analyzedAt":"2026-08-13T04:48:44.121Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}