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
- Check that maintenance mode is actually active before calling maintenanceLogin (call the status endpoint first).
- If maintenance is done, stop attempting maintenance login and use the normal authentication flow instead.
- Restart maintenance mode via the setAction endpoint (POST /maintenance) with the correct mode if you still need maintenance access.
- 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
- Query the maintenance status endpoint before attempting maintenance login.
- Use the normal auth flow once maintenance mode has ended.
- Point clients at the correct server instance (normal installs always reject this endpoint).
- Handle stale maintenance sessions in the UI by refreshing status.
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
- Invalid license key
- Shared link is not password protected
- This endpoint can only be used with a session token
- Asset dimensions are not available for editing
- Asset does not have valid dimensions
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)