{"record":{"id":"df6bc74d66700b80","repo":"rohitg00/agentmemory","slug":"viewer-api-fetchopts-method-get-path","errorCode":null,"errorMessage":"[viewer] API ${fetchOpts.method || 'GET'} ${path} returned ${res.status}","messagePattern":"\\[viewer\\] API (.+?) (.+?) returned (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/viewer/index.html","lineNumber":1352,"sourceCode":"      host.classList.remove('open');\n      host.innerHTML = '';\n    }\n\n    async function api(path, opts) {\n      try {\n        var url = REST + '/agentmemory/' + path;\n        var headers = Object.assign({ 'Cache-Control': 'no-cache' }, (opts && opts.headers) || {});\n        var viewerToken = getViewerToken();\n        if (viewerToken && !headers.Authorization && !headers.authorization) {\n          headers.Authorization = 'Bearer ' + viewerToken;\n        }\n        var fetchOpts = Object.assign({}, opts || {}, { headers: headers });\n        var readErrorBody = fetchOpts.readErrorBody;\n        delete fetchOpts.readErrorBody;\n        var res = await fetch(url, fetchOpts);\n        if (!res.ok) {\n          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;","sourceCodeStart":1334,"sourceCodeEnd":1370,"githubUrl":"https://github.com/rohitg00/agentmemory/blob/e04ba88819c365c9acf9d6661ea802143e728bd6/src/viewer/index.html#L1334-L1370","documentation":"This is a console.warn emitted by the agentmemory viewer's central `api()` helper (src/viewer/index.html:1338-1372) whenever a REST call to the local agentmemory daemon resolves with a non-2xx HTTP status. It is not a thrown exception: the helper logs the method, path, and status, then resolves to `null` so callers can uniformly treat null as 'request failed'. A 401 additionally opens the viewer auth prompt. Because it's a warning, the browser console shows it but the page continues running in a degraded state.","triggerScenarios":"Any `api()`/`apiGet()`/`apiPost()` call where `fetch(REST + '/agentmemory/' + path)` returns `res.ok === false`: daemon not running (connection succeeds via proxy but 502/503), 401 when the viewer token is missing/expired/rotated (AGENTMEMORY_VIEWER_TOKEN mismatch), 404 when the endpoint path or tool count changed across versions, 503 from the health endpoint when status is 'critical', or 4xx/5xx from malformed POST bodies.","commonSituations":"Developer opens the viewer dashboard while the agentmemory daemon is down or restarted with a different port; the stored viewer token was invalidated after `AGENTMEMORY_SECRET` changed so every call returns 401 and the auth prompt reappears; a stale cached index.html calls an endpoint renamed in a newer version (404); the health badge intentionally logs 503 when system status is critical.","solutions":["Check the daemon is up: `curl http://localhost:49134/agentmemory/health` (or your AGENTMEMORY_URL) and restart it if unreachable.","If the warn is 401, re-enter the viewer token in the auth prompt, or verify the Authorization header / getViewerToken() value matches the daemon's expected token.","If 404, confirm the endpoint path exists in your installed version (README REST endpoint list) and hard-refresh to clear a stale cached index.html.","If 502/503 via a reverse proxy, fix the proxy upstream target so it points at the daemon's actual port.","In caller code, handle the resolved `null` (e.g. loadGraph's error/disabled state) instead of assuming data — the helper never rejects."],"exampleFix":"// before: assuming data is always present\nconst graph = await apiGet('graph');\nrender(graph.nodes); // TypeError if request returned 401/500 -> null\n\n// after: handle null as 'request failed'\nconst graph = await apiGet('graph');\nif (!graph) {\n  showViewerErrorState('Failed to load graph — is the daemon running?');\n  return;\n}\nrender(graph.nodes);","handlingStrategy":"fallback","validationCode":"// Probe before relying on the API\nasync function isApiHealthy(baseUrl) {\n  try {\n    const res = await fetch(baseUrl + '/agentmemory/health', {\n      signal: AbortSignal.timeout(3000),\n    });\n    return res.ok || res.status === 503; // 503 with JSON body is still valid (see #1019)\n  } catch {\n    return false;\n  }\n}","typeGuard":"function isApiResult<T>(result: T | null): result is T {\n  return result !== null && typeof result === 'object';\n}\n\n// usage\nconst data = await apiGet('graph');\nif (isApiResult(data)) { render(data.nodes); }","tryCatchPattern":"// The helper resolves null instead of rejecting, so guard the result;\n// wrap the call only to also catch network-level fetch rejections.\ntry {\n  const data = await apiPost('memory_search', { query });\n  if (data === null) {\n    // non-2xx: check console warn status (401 -> re-auth, 5xx -> daemon issue)\n    showFallbackUi();\n  } else {\n    render(data);\n  }\n} catch (err) {\n  // fetch itself rejected (daemon down, CORS, aborted)\n  console.warn('viewer request failed:', err);\n  showFallbackUi();\n}","preventionTips":["Always null-check the return value of api()/apiGet()/apiPost() — non-2xx resolves to null by design.","Use readErrorBody: true (as the health badge does) when a non-2xx response still carries a usable JSON body.","Keep the viewer token in sync with the daemon's secret; a rotated AGENTMEMORY_SECRET invalidates stored tokens (401).","Poll /agentmemory/health on an interval and show a daemon-down banner before other calls fail.","Hard-refresh the viewer after upgrading agentmemory so stale cached HTML doesn't call renamed endpoints."],"tags":["http","network","fetch","viewer","non-2xx"],"backgroundTag":"http-non-2xx-response","analyzedSha":"e04ba88819c365c9acf9d6661ea802143e728bd6","analyzedAt":"2026-08-30T01:07:40.754Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}