{"record":{"id":"0dcc223373183bc4","repo":"rohitg00/agentmemory","slug":"viewer-api-error-on-path","errorCode":null,"errorMessage":"[viewer] API error on ${path}:","messagePattern":"\\[viewer\\] API error on (.+?):","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/viewer/index.html","lineNumber":1369,"sourceCode":"          if (res.status === 401) showViewerAuthPrompt();\n          console.warn('[viewer] API ' + (fetchOpts.method || 'GET') + ' ' + path + ' returned ' + res.status);\n          // Non-2xx responses resolve to null so callers can keep treating\n          // null as \"request failed\" (e.g. loadGraph's disabled/error state).\n          // The health endpoint opts out via readErrorBody: it intentionally\n          // responds 503 with a valid JSON body when status is \"critical\"\n          // (see #1019), and the dashboard badge needs that body.\n          if (!readErrorBody) return null;\n          try {\n            return await res.json();\n          } catch (parseErr) {\n            console.debug('[viewer] API ' + path + ' non-2xx body was not JSON:', parseErr);\n            return null;\n          }\n        }\n        hideViewerAuthPrompt();\n        return await res.json();\n      } catch (err) {\n        console.warn('[viewer] API error on ' + path + ':', err);\n        return null;\n      }\n    }\n    async function apiGet(path) { return api(path); }\n    async function apiPost(path, body) {\n      return api(path, {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify(body || {})\n      });\n    }\n    async function apiDelete(path, body) {\n      return api(path, {\n        method: 'DELETE',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify(body || {})\n      });\n    }","sourceCodeStart":1351,"sourceCodeEnd":1387,"githubUrl":"https://github.com/rohitg00/agentmemory/blob/e04ba88819c365c9acf9d6661ea802143e728bd6/src/viewer/index.html#L1351-L1387","documentation":"The viewer's shared `api(path, ...)` fetch wrapper catches any network/HTTP failure, logs `[viewer] API error on <path>:` with the error, and returns null, which the UI renders as empty data rather than a crash. It fires when fetch itself rejects (daemon down, CORS, aborted request) or when the surrounding code throws before res.json() succeeds (e.g. non-JSON response from a proxy or an auth redirect).","triggerScenarios":"Raised in src/viewer/index.html:1369 whenever apiGet/apiPost is called while the agentmemory daemon is unreachable, the viewer auth prompt was bypassed with an invalid token (401 handling falls through), a proxy intercepts the request and returns non-JSON, or the request is aborted.","commonSituations":"Daemon not running or restart while the viewer tab stays open; AGENTMEMORY_URL / viewer port misconfigured; a reverse proxy serving an HTML login page instead of JSON; viewer auth token wrong so every call fails; browser blocking mixed content or CORS.","solutions":["Confirm the daemon is running and the viewer/REST port matches: curl the same path on the daemon (e.g. curl http://localhost:<port>/agentmemory/health).","Check the browser console/network tab for the actual failing request — status code or CORS/redirect cause.","Re-authenticate with the viewer token if API calls return 401 (the auth prompt appears again).","Fix AGENTMEMORY_URL / proxy configuration so the path returns JSON, not an HTML error page.","Restart the viewer page after restarting the daemon so it picks up the correct base URL."],"exampleFix":"// before\nconst data = await apiGet('/agentmemory/sessions');\nrender(data.rows); // crashes on null\n// after\nconst data = await apiGet('/agentmemory/sessions');\nif (!data) { showEmptyState('Could not reach agentmemory daemon'); return; }\nrender(data.rows);","handlingStrategy":"fallback","validationCode":"async function daemonReachable(base) {\n  try { const r = await fetch(base + '/agentmemory/health', { signal: AbortSignal.timeout(3000) }); return r.ok; }\n  catch { return false; }\n}","typeGuard":"function isApiPayload(v) {\n  return v !== null && typeof v === 'object' && !Array.isArray(v);\n}\nconst data = await apiGet(path);\nif (!isApiPayload(data)) showEmptyState('daemon unreachable or returned non-JSON');","tryCatchPattern":"// the viewer api() wrapper already try/catches; consume its null return defensively\nconst data = await api(path);\nif (data === null) {\n  renderError('Could not reach the agentmemory daemon — is it running?');\n  return;\n}","preventionTips":["Check daemon health (curl /agentmemory/health) before troubleshooting viewer UI emptiness.","Keep the daemon and viewer on the same agentmemory version; restart the viewer tab after daemon restarts.","Ensure proxies in front of the daemon return JSON (not HTML login/error pages) for /agentmemory/* paths.","Enter a valid viewer auth token when prompted to avoid persistent 401s.","Watch the browser console for CORS/mixed-content errors if accessing the viewer cross-origin."],"tags":["http","viewer","fetch","null-response","frontend"],"backgroundTag":"fetch-failed-null-response","analyzedSha":"e04ba88819c365c9acf9d6661ea802143e728bd6","analyzedAt":"2026-08-30T01:07:40.754Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}