{"record":{"id":"21eb78e5c2e8b7af","repo":"immich-app/immich","slug":"expected-a-json-response","errorCode":null,"errorMessage":"Expected a JSON response","messagePattern":"Expected a JSON response","errorType":"exception","errorClass":"MalformedResponseError","httpStatus":null,"severity":"error","filePath":"packages/sdk/src/index.ts","lineNumber":66,"sourceCode":"    throw new Error('The API key header can only be set using setApiKey().');\n  }\n};\n\nexport const jsonOnly =\n  (impl?: typeof fetch): typeof fetch =>\n  async (input, options) => {\n    const response = await (impl ?? fetch)(input, options);\n\n    const expectsJson = new Headers(options?.headers)\n      .get('accept')\n      ?.includes('json');\n    if (!expectsJson || response.status === 204) {\n      return response;\n    }\n\n    const contentType = response.headers.get('content-type');\n    if (!contentType?.includes('json')) {\n      throw new MalformedResponseError(\n        'Expected a JSON response',\n        response.url,\n        response.status,\n        contentType,\n      );\n    }\n\n    return response;\n  };\n\nexport const setFetch = (impl: typeof fetch) => {\n  defaults.fetch = jsonOnly(impl);\n};\n\ndefaults.fetch = jsonOnly();\n\nexport const getAssetOriginalPath = (id: string) => `/assets/${id}/original`;\n","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/immich-app/immich/blob/e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c/packages/sdk/src/index.ts#L48-L84","documentation":"The SDK's jsonOnly fetch wrapper expects every non-204 response to a JSON-requested endpoint to have a JSON content-type. If the server returns non-JSON (HTML login page, empty error body, wrong route), MalformedResponseError('Expected a JSON response') is thrown carrying the URL, status, and content-type so the caller can diagnose what the server actually returned.","triggerScenarios":"Requesting an Immich endpoint whose response lacks 'json' in content-type: hitting an HTML 404/402 page, a proxy error page, or a route returning text/plain; also wrong base URL pointing at a non-Immich server.","commonSituations":"Base URL typo or reverse proxy intercepting the request (auth portal, Cloudflare challenge), server returning an HTML error for a missing route, or an old SDK talking to an endpoint that changed its response type.","solutions":["Check the URL/status/content-type embedded in the MalformedResponseError to see what the server actually replied with.","Verify the base URL points at your Immich instance's API (no trailing path mistakes) and there is no auth portal intercepting requests.","Inspect the response outside the SDK (curl -i) to see the raw body.","Upgrade SDK and server to matching versions if an endpoint's content type changed."],"exampleFix":"// before\nnew ImmichApi({ baseUrl: 'https://my-proxy.example.com' }) // proxy serves HTML\n// after\nnew ImmichApi({ baseUrl: 'https://immich.example.com/api' }) // direct JSON API","handlingStrategy":"try-catch","validationCode":"const res = await fetch(`${baseUrl}/server-info/ping`);\nconst ct = res.headers.get('content-type') ?? '';\nif (!ct.includes('json')) throw new Error(`baseUrl returns non-JSON (${ct}) - check URL/proxy`);","typeGuard":null,"tryCatchPattern":"try {\n  await api.someCall();\n} catch (e) {\n  if (e instanceof MalformedResponseError) {\n    console.error(`non-JSON from ${e.url} (status ${e.status}, ct ${e.contentType}); check baseUrl/proxy`);\n  } else throw e;\n}","preventionTips":["Ping /server-info/ping at startup to confirm the endpoint speaks JSON","Point baseUrl directly at the Immich API, bypassing auth portals/CDN challenges","Keep SDK and server versions in sync"],"tags":["http","content-type","json","sdk"],"backgroundTag":"unexpected-response-shape","analyzedSha":"e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c","analyzedAt":"2026-09-15T07:20:19.675Z","contentChangedAt":"2026-09-15T07:20:19.675Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}