immich-app/immich · error · BadRequestException

Not in maintenance mode

Error message

Not in maintenance mode

What it means

This BadRequestException is thrown by the maintenanceLogin endpoint (POST /maintenance/login) whenever the server is not currently in maintenance mode. The endpoint is a stub that unconditionally rejects login attempts unless maintenance mode was already activated; its purpose is only to let an already-maintenancing server hand out maintenance auth. It is always a 400 response with the message 'Not in maintenance mode'.

Solutions

  1. Check that maintenance mode is actually active before calling maintenanceLogin (call the status endpoint first).
  2. If maintenance is done, stop attempting maintenance login and use the normal authentication flow instead.
  3. Restart maintenance mode via the setAction endpoint (POST /maintenance) with the correct mode if you still need maintenance access.
  4. Verify you are hitting the right server/port — a normal (non-maintenance) Immich instance will always return this error.

Example fix

// before
class Client {
  async maintenanceLogin(token: string) {
    return this.post('/maintenance/login', { token }); // 400 Not in maintenance mode
  }
}
// after
class Client {
  async maintenanceLogin(token: string) {
    const status = await this.get('/maintenance/status');
    if (!status.active) {
      throw new Error('Server is not in maintenance mode; skipping maintenance login');
    }
    return this.post('/maintenance/login', { token });
  }
}
Defensive patterns

Strategy: try-catch

Validate before calling

const status = await fetch(`${base}/maintenance/status`).then(r => r.json());
if (!status.active) {
  throw new Error('Server not in maintenance mode; maintenance login unavailable');
}

Type guard

function isMaintenanceActive(s: unknown): s is { active: true } {
  return typeof s === 'object' && s !== null && (s as { active?: unknown }).active === true;
}

Try / catch

try {
  await api.post('/maintenance/login', dto);
} catch (e) {
  if (e instanceof BadRequestException && e.message === 'Not in maintenance mode') {
    // fall back to normal login flow
  } else throw e;
}

Prevention

When it happens

Trigger: Calling POST /maintenance/login (MaintenanceLoginDto body) against an Immich server whose maintenance mode is not active (this.setStatus({active:false}) or never activated). Any request to this endpoint while the server runs normally returns this error.

Common situations: Clients or scripts attempting to log into the maintenance web UI after maintenance mode already ended (setAction cleared it); hitting the maintenance endpoint on a normal production install; a race where the server exited maintenance mode before the login request arrived; stale browser tab pointing at a server that rebooted out of maintenance.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15). Data as JSON: /api/errors/65346ab616d4eef3. Report an issue: GitHub.

Appendix: source

Thrown at server/src/controllers/maintenance.controller.ts:54

  @Endpoint({
    summary: 'Detect existing install',
    description: 'Collect integrity checks and other heuristics about local data.',
    history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'),
  })
  @Authenticated({ permission: Permission.Maintenance, admin: true })
  detectPriorInstall(): Promise<MaintenanceDetectInstallResponseDto> {
    return this.service.detectPriorInstall();
  }

  @Post('login')
  @Endpoint({
    summary: 'Log into maintenance mode',
    description: 'Login with maintenance token or cookie to receive current information and perform further actions.',
    history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
  })
  @Authenticated({ public: true })
  maintenanceLogin(@Body() _dto: MaintenanceLoginDto): MaintenanceAuthDto {
    throw new BadRequestException('Not in maintenance mode');
  }

  @Post()
  @Endpoint({
    summary: 'Set maintenance mode',
    description: 'Put Immich into or take it out of maintenance mode',
    history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
  })
  @Authenticated({ permission: Permission.Maintenance, admin: true })
  async setMaintenanceMode(
    @Auth() auth: AuthDto,
    @Body() dto: SetMaintenanceModeDto,
    @GetLoginDetails() loginDetails: LoginDetails,
    @Res({ passthrough: true }) res: Response,
  ): Promise<void> {
    if (dto.action === MaintenanceAction.End) {
      return;
    }

View on GitHub (pinned to e55ac299a4)