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

  1. Deploy the frontend build output (www/index.html) alongside the server if you want SSR.
  2. If using the server-only image intentionally, ignore the warning — SPA mode still works.
  3. Check IMMICH_ resource-path configuration so resourcePaths.web.indexHtml matches the actual build location.
  4. 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

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


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)