immich-app/immich · warning
Unable to open , skipping SSR.
Error message
Unable to open ${resourcePaths.web.indexHtml}, skipping SSR. What it means
The SSR middleware reads the frontend's index.html at startup to render pages server-side. If the file cannot be read, SSR is skipped and every page request falls back to the plain SPA served statically. The warning is logged once when ssr() builds the middleware.
Solutions
- Deploy the frontend build output (www/index.html) alongside the server if you want SSR.
- If using the server-only image intentionally, ignore the warning — SPA mode still works.
- Check IMMICH_ resource-path configuration so resourcePaths.web.indexHtml matches the actual build location.
- Re-run/repair the deployment so the web assets are copied completely.
Example fix
// before
index = readFileSync(resourcePaths.web.indexHtml).toString();
// after
if (existsSync(resourcePaths.web.indexHtml)) {
index = readFileSync(resourcePaths.web.indexHtml).toString();
} else {
this.logger.warn(`SSR disabled: ${resourcePaths.web.indexHtml} not found`);
} Defensive patterns
Strategy: fallback
Validate before calling
import { existsSync } from 'node:fs';
const ssrAvailable = existsSync(resourcePaths.web.indexHtml); Try / catch
let index = '';
try {
index = readFileSync(indexHtmlPath, 'utf8');
} catch {
logger.warn(`SSR disabled: ${indexHtmlPath} missing; serving SPA`);
} Prevention
- Deploy web build output together with the server when SSR is desired.
- Keep resourcePaths config in sync with actual build layout.
- Confirm assets copied fully in deployment scripts (check index.html exists post-deploy).
When it happens
Trigger: readFileSync(resourcePaths.web.indexHtml) throws when building the SSR middleware — index.html missing or unreadable at server startup.
Common situations: Server-only Docker image (immich-server without web build output); custom reverse-proxy/static hosting of the frontend; partial or interrupted deployment where the web assets directory is absent.
Understand the failure class
Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.
Related errors
- Attempted to clear cache, but rmtree is not safe on this…
- Device ' ' does not exist. If using Docker, make sure this…
- Failed to add job handler for
- Failed to create
- Failed to find job handler for Job.
AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15).
Data as JSON: /api/errors/3009fe907938e944.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/api.service.ts:46
@Injectable()
export class ApiService {
constructor(
private authService: AuthService,
private sharedLinkService: SharedLinkService,
private configRepository: ConfigRepository,
private logger: LoggingRepository,
) {
this.logger.setContext(ApiService.name);
}
ssr(excludePaths: string[]) {
const { resourcePaths } = this.configRepository.getEnv();
let index = '';
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`,
);
}View on GitHub (pinned to e55ac299a4)