{"record":{"id":"42f87b4042422ab2","repo":"paperclipai/paperclip","slug":"provider-returned-an-unreadable-inventory-respo","errorCode":null,"errorMessage":"${provider} returned an unreadable inventory response","messagePattern":"(.+?) returned an unreadable inventory response","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/chat-provider-inventory.ts","lineNumber":49,"sourceCode":"const GITHUB_API_TIMEOUT_MS = 25_000;\n\nfunction slackRequestSignal(): AbortSignal {\n  return AbortSignal.timeout(SLACK_API_TIMEOUT_MS);\n}\n\nfunction githubRequestSignal(): AbortSignal {\n  return AbortSignal.timeout(GITHUB_API_TIMEOUT_MS);\n}\n\nasync function jsonResponse<T>(\n  response: Response,\n  provider: string,\n): Promise<T> {\n  let body: unknown;\n  try {\n    body = await response.json();\n  } catch {\n    throw new Error(`${provider} returned an unreadable inventory response`);\n  }\n  if (!response.ok) {\n    const message =\n      body && typeof body === \"object\" && \"message\" in body\n        ? String((body as { message?: unknown }).message)\n        : String(response.status);\n    throw new Error(`${provider} inventory failed: ${message}`);\n  }\n  return body as T;\n}\n\n/** List only Slack conversations where the installed bot is a member. */\nexport async function listSlackBotChannels(input: {\n  botToken: string;\n  fetch: typeof globalThis.fetch;\n}): Promise<ChatProviderInventoryResult> {\n  const resources: ChatProviderResourceInventoryItem[] = [];\n  let cursor = \"\";","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-provider-inventory.ts#L31-L67","documentation":"Thrown by jsonResponse, the shared inventory-response helper in chat-provider-inventory.ts, when response.json() rejects — i.e. the provider (GitHub or Slack) returned a body that is not parseable JSON. The helper is used by all chat-provider inventory listings, so this guards every provider call uniformly.","triggerScenarios":"response.json() throws on: HTML error page from a proxy/captive portal, empty body, malformed JSON, or a stream read error even though the HTTP exchange completed.","commonSituations":"Corporate proxy or VPN intercepting provider API traffic; provider outage returning an error page with 200/5xx; network middleware truncating responses; calling through a URL that actually returns HTML (wrong base URL config).","solutions":["Retry the inventory request; transient truncation usually resolves.","Verify the server can reach the provider API directly (curl the endpoint) without HTML-interposing proxies.","Check the provider's base URL configuration for typos or outdated endpoints.","Inspect provider status pages (Slack/GitHub) for ongoing incidents."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Sanity-check reachability/JSON before inventory:\nconst probe = await fetch(providerUrl, { method: 'HEAD' }).catch(() => null);\nif (!probe || (probe.headers.get('content-type') ?? '').includes('text/html')) {\n  throw new Error('Provider endpoint unreachable or returning HTML; check proxy/base URL');\n}","typeGuard":null,"tryCatchPattern":"try { const inv = await listProviderResources(input); }\ncatch (e) {\n  if (e.message.includes('unreadable inventory response')) {\n    await retryWithBackoff(() => listProviderResources(input), 3); // transient bad body\n  } else throw e;\n}","preventionTips":["Verify provider base URLs and content types expected (application/json).","Bypass HTML-injecting proxies/captive portals for provider API hosts.","Retry transient body errors with backoff.","Monitor provider status pages."],"tags":["json","http","inventory","response-parsing"],"backgroundTag":"invalid-json-response","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}