immich-app/immich · error · MalformedResponseError
Expected a JSON response
Error message
Expected a JSON response
What it means
The SDK wraps its fetch implementation with `jsonOnly`, a fetch middleware that verifies a response actually is JSON whenever the request's Accept header asks for JSON (and the status isn't 204). If the response's Content-Type header is missing or does not include 'json', a MalformedResponseError('Expected a JSON response') is thrown, carrying the URL, status, and content type. This guards callers against receiving HTML error pages, empty bodies, or binary data when a typed JSON response is expected.
Source
Thrown at packages/sdk/src/index.ts:66
throw new Error('The API key header can only be set using setApiKey().');
}
};
export const jsonOnly =
(impl?: typeof fetch): typeof fetch =>
async (input, options) => {
const response = await (impl ?? fetch)(input, options);
const expectsJson = new Headers(options?.headers)
.get('accept')
?.includes('json');
if (!expectsJson || response.status === 204) {
return response;
}
const contentType = response.headers.get('content-type');
if (!contentType?.includes('json')) {
throw new MalformedResponseError(
'Expected a JSON response',
response.url,
response.status,
contentType,
);
}
return response;
};
export const setFetch = (impl: typeof fetch) => {
defaults.fetch = jsonOnly(impl);
};
defaults.fetch = jsonOnly();
export const getAssetOriginalPath = (id: string) => `/assets/${id}/original`;
View on GitHub (pinned to 5666d57f15)
Solutions
- Check response.url/status/contentType in the MalformedResponseError to see what actually came back; usually the baseUrl is wrong or a proxy returned an HTML page.
- Verify baseUrl points at the API server root (not the web app) and is reachable without browser redirects.
- Check proxy/WAF/anti-bot layers (nginx, Cloudflare) and either fix them or add an allowlist for the API path.
- If you intentionally call a non-JSON endpoint (e.g. binary asset download), omit the Accept: application/json header from the request so jsonOnly skips the check.
- If using a custom fetch via setFetch, ensure it forwards the upstream Content-Type header unchanged.
Example fix
// before
init({ baseUrl: 'https://my-immich.example.com' }); // hits web app, gets HTML
// after
init({ baseUrl: 'https://my-immich.example.com/api' }); // hits JSON API Defensive patterns
Strategy: try-catch
Validate before calling
// check what the server actually returns before trusting JSON
const res = await fetch(url, { headers: { accept: 'application/json' } });
const ct = res.headers.get('content-type') ?? '';
if (!ct.includes('json')) console.warn(`non-JSON from ${res.url}: ${ct}, status ${res.status}`); Type guard
function isMalformedResponseError(e: unknown): e is MalformedResponseError {
return e instanceof MalformedResponseError;
} Try / catch
try {
const res = await sdk.someJsonCall();
} catch (e) {
if (isMalformedResponseError(e)) {
console.error(`Non-JSON response from ${e.url} (status ${e.status}, content-type ${e.contentType})`);
} else throw e;
} Prevention
- Point baseUrl at the JSON API root, not the web UI.
- Keep Accept: application/json only on endpoints that truly return JSON; drop it for binary asset downloads.
- Check proxy/WAF rules that replace API responses with HTML error pages.
- When wrapping fetch with setFetch, never strip or rewrite the Content-Type header.
- Inspect e.url/e.status/e.contentType on the error before retrying blindly.
When it happens
Trigger: Any SDK call that sends Accept: application/json but whose server response Content-Type is not JSON — e.g. the server returns an HTML error/interstitial page (reverse proxy, login portal, Cloudflare block page), returns an empty body with text/plain, redirects to a non-JSON endpoint, or a custom fetch impl (setFetch) that mislabels or strips Content-Type.
Common situations: Wrong baseUrl pointing at a web UI instead of the API; a proxy or WAF intercepting requests and returning HTML 403/502 pages; misconfigured server behind auth that responds with a redirect to a login page; custom fetch implementations (via setFetch) that swallow or rewrite headers.
Related errors
- Failed to fetch picture: ${response.statusText}
- The API key header can only be set using setApiKey().
- The route ${request.path} was requested as ${request.header(
- Failed to fetch activation key
- Failed to verify SMTP configuration
AI-assisted analysis of immich-app/immich@5666d57f15 (2026-09-01).
Data as JSON: /api/errors/21eb78e5c2e8b7af.
Report an issue: GitHub.