{"record":{"id":"1621ee5c945bb58e","repo":"chatboxai/chatbox","slug":"pdf-password-protected","errorCode":"pdf_password_protected","errorMessage":"pdf_password_protected","messagePattern":"pdf_password_protected","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/file-parser.ts","lineNumber":243,"sourceCode":"      }\n    }\n\n    if (pageTexts.every((text) => text === '')) {\n      // No extractable text (e.g. a scanned PDF) — keep returning an empty string\n      // instead of a list of bare page markers.\n      return ''\n    }\n\n    log.info(`Parsed PDF ${filePath}: ${document.numPages} pages`)\n    return pageTexts.map((text, i) => `${formatPdfPageMarker(i + 1)}\\n\\n${text}`).join('\\n\\n')\n  } catch (error) {\n    // pdfjs throws a typed PasswordException for encrypted PDFs. Surface it as a\n    // distinct code so the user is told the PDF needs a password instead of the\n    // generic \"unsupported file\" message (and so the cloud fallback, which also\n    // cannot read it, is skipped). Per-page failures are handled inside the loop\n    // and never reach here.\n    if (error instanceof Error && error.name === 'PasswordException') {\n      throw new Error(LOCAL_PARSER_PDF_PASSWORD_PROTECTED_ERROR)\n    }\n    throw error\n  } finally {\n    await loadingTask.destroy()\n  }\n}\n\nexport async function parseEpub(filePath: string): Promise<string> {\n  return new Promise((resolve, reject) => {\n    const epub = new Epub(filePath)\n\n    epub.on('error', (error) => {\n      log.error('EPUB parsing error:', error)\n      reject(error)\n    })\n\n    epub.on('end', async () => {\n      try {","sourceCodeStart":225,"sourceCodeEnd":261,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/main/file-parser.ts#L225-L261","documentation":"Thrown by CustomClaude.listModels() when GET `${apiHost}/models?limit=990` returns a body without a top-level `data` array. Identical shape-guard to the built-in Claude provider, but here the host is a user-supplied `apiHost` (a custom Anthropic-compatible endpoint) and the request is sent through the optional proxy with a fixed `x-api-key` header. The missing `data` field means the custom endpoint is not speaking the Anthropic `/v1/models` schema.","triggerScenarios":"A custom provider configured with an OpenAI-compatible base URL mistaken for an Anthropic-compatible one (returns `{ data: [...] }` is actually correct for Anthropic, but an OpenAI proxy returning `{ models: [...] }` fails); `apiHost` includes a trailing `/v1` or a path segment so the constructed URL is malformed; `apiKey` is wrong and the upstream returns `{ type: 'error', error: { ... } }`; the proxy rewrites the response into HTML or a generic error envelope.","commonSituations":"User adds a 'Custom Claude' provider pointing at a relay that only implements `/v1/messages` (chat) but not `/v1/models`; apiHost pasted with a trailing slash or `/v1` suffix that normalizeClaudeHost then double-prefixes; key from a different vendor reused; reverse proxy (nginx/cloudflare) returns a 502 HTML page that res.json() parses to `{}` or throws.","solutions":["Confirm the custom host actually implements the Anthropic `/v1/models` endpoint with `{ data: [{ id, type }] }` — many Claude relays only proxy chat.","Read the JSON.stringify'd payload inside the ApiError message: an `error.type` of `authentication_error` means bad key; an `overloaded_error` is transient; an object with `models` instead of `data` means wrong schema.","Set `apiHost` to the base origin only (e.g. `https://relay.example.com`), no `/v1` suffix — normalizeClaudeHost appends the version path.","If the relay is OpenAI-style, switch the provider type from 'Custom Claude' to a generic Custom OpenAI provider."],"exampleFix":"// before: relay returns { models: [...] }\n// after: either expose /v1/models with Anthropic schema, or define models manually\nasync listModels() {\n  try { return await super.listModels() }\n  catch (e) {\n    if (e instanceof ApiError) return [{ modelId: this.options.model.modelId, type: 'chat' }]\n    throw e\n  }\n}","handlingStrategy":"try-catch","validationCode":"async function probeCustomClaudeModels(host: string, key: string): Promise<boolean> {\n  const res = await fetch(`${host}/models?limit=990`, {\n    headers: { 'anthropic-version': '2023-06-01', 'x-api-key': key }\n  })\n  const json = await res.json().catch(() => ({}))\n  return Array.isArray((json as any)?.data)\n}","typeGuard":"function isAnthropicModelsResponse(json: unknown): json is { data: { id: string; type: string }[] } {\n  return typeof json === 'object' && json !== null && Array.isArray((json as any).data)\n}","tryCatchPattern":"try { return await customClaude.listModels() }\ncatch (e) {\n  if (e instanceof ApiError) return [{ modelId: customClaude.options.model.modelId, type: 'chat' }]\n  throw e\n}","preventionTips":["When adding a Custom Claude provider, probe /models during setup and warn if the schema is wrong.","Document that Custom Claude relays must implement the Anthropic /v1/models route, not OpenAI /v1/models.","Trim apiHost and strip any trailing /v1 to avoid double-prefixed URLs."],"tags":["api","anthropic","custom-provider","claude","models-list","proxy"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}