immich-app/immich · warning · NotAcceptableException
The route ${request.path} was requested as ${request.header(
Error message
The route ${request.path} was requested as ${request.header('accept')}, but only returns text/html What it means
Thrown by the ApiService.ssr Express middleware as NotAcceptableException (HTTP 406) when a non-/api GET/HEAD request (not on an excluded path) does not accept text/html. The middleware powers server-side rendering of the web app and Open Graph tags for share links, so it only serves HTML. Any client that demands JSON (e.g. Accept: application/json) on a share or root route trips this.
Source
Thrown at server/src/services/api.service.ts:61
try {
index = readFileSync(resourcePaths.web.indexHtml).toString();
} catch {
this.logger.warn(`Unable to open ${resourcePaths.web.indexHtml}, skipping SSR.`);
}
return async (request: Request, res: Response, next: NextFunction) => {
const method = request.method.toLowerCase();
if (
request.url.startsWith('/api') ||
(method !== 'get' && method !== 'head') ||
excludePaths.some((item) => request.url.startsWith(item))
) {
return next();
}
const responseType = request.accepts('text/html');
if (!responseType) {
throw new NotAcceptableException(
`The route ${request.path} was requested as ${request.header('accept')}, but only returns text/html`,
);
}
let status = 200;
let html = index;
const defaultDomain = request.host ? `${request.protocol}://${request.host}` : undefined;
let meta: OpenGraphTags | null = null;
const shareKey = request.url.match(/^\/share\/(.+)$/);
if (shareKey) {
try {
const key = shareKey[1];
const auth = await this.authService.validateSharedLinkKey(key);
meta = await this.sharedLinkService.getMetadataTags(auth, defaultDomain);
} catch {View on GitHub (pinned to 199723261c)
Solutions
- Send `Accept: text/html` (or `*/*`) when fetching share pages or any non-/api route
- Use the /api/* endpoints for machine-readable data instead of the SSR routes
- Configure programmatic clients to vary Accept by route family
Example fix
// before
await fetch(`${origin}/share/abc`, { headers: { accept: 'application/json' } }); // 406
// after
await fetch(`${origin}/share/abc`, { headers: { accept: 'text/html' } }); Defensive patterns
Strategy: validation
Validate before calling
// Set Accept correctly for SSR vs API routes
const headers = isShareRoute(url) ? { accept: 'text/html' } : { accept: 'application/json' };
await fetch(url, { headers }); Type guard
function acceptsHtml(accept: string | undefined): boolean {
if (!accept) return true; // default
return /text\/html|\*\/\*/i.test(accept);
} Try / catch
try {
return await fetch(url, { headers: { accept: 'text/html' } });
} catch (e) {
if (e instanceof HttpError && e.status === 406) {
// retry with explicit text/html, or fall back to /api route
}
throw e;
} Prevention
- Default programmatic clients to Accept: */* to avoid 406
- Pin Accept: text/html only for share/root SSR routes
- Use /api/* paths for JSON data — never scrape SSR routes for structured data
When it happens
Trigger: GET /share/:key, /, or any non-/api GET route with an Accept header that excludes text/html — e.g. `Accept: application/json`, `Accept: image/*`, or a strict `Accept` from a programmatic client/scraper.
Common situations: Curl/HTTPie defaults that send `Accept: */*` work, but clients that pin `Accept: application/json` for all calls get 406; bots/scrapers with restrictive Accept; SDK code reused for share-link fetching that assumes JSON.
Related errors
- Cannot grant permissions you do not have
- API Key not found
- Not authenticated with an API Key
- Unsupported file type ${filename}
- May not request original file
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/c3502b882068f7da.
Report an issue: GitHub.