immich-app/immich · error · BadRequestException
Invalid ack type
Error message
Invalid ack type: ${type} What it means
setAcks accepts client acknowledgements for known sync entity types, but there is no class-validator on the payload (per the TODO), so it manually checks the type against the SyncEntityType enum and throws BadRequestException for unknown values.
Solutions
- Update the Immich client to match the server version so SyncEntityType values align
- Inspect the ack payload and remove/correct entries whose type is not a valid SyncEntityType
- If stale, trigger a sync reset so the client rebuilds its ack state
Example fix
// before
await setAcks([{ type: 'AssetV1', ack: '...' }])
// after
await setAcks([{ type: SyncEntityType.AssetV1, ack: '...' }]) Defensive patterns
Strategy: validation
Validate before calling
const validTypes = Object.values(SyncEntityType);
const isValidAck = (a) => validTypes.includes(a.type);
acks.forEach(a => { if (!isValidAck(a)) throw new Error(`Invalid ack type: ${a.type}`); }); Type guard
const isValidSyncEntityType = (t) => Object.values(SyncEntityType).includes(t);
Try / catch
try {
await syncClient.setAcks(acks);
} catch (e) {
if (e.status === 400 && e.message.startsWith('Invalid ack type')) {
acks = acks.filter(a => isValidSyncEntityType(a.type)); // drop stale types
}
} Prevention
- Filter client acks against the server's SyncEntityType enum before sending
- Keep client and server versions aligned
- Reset sync state after server downgrades
When it happens
Trigger: POSTing acks to the sync ack endpoint containing a type string not present in SyncEntityType (typo, newer client on older server, fabricated type). Special case: SyncResetV1 is handled before this check and resets progress instead of being recorded.
Common situations: Version mismatch between mobile client and server after downgrade; hand-crafted API requests; corrupted local ack database sending stale/invalid types.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- assetIds, albumId, or userId is required
- At least two people are required for merging
- Cannot request to join your own cluster group
- error instanceof Error ? error.message : error
- {error.message}
AI-assisted analysis of immich-app/immich@f48d4b3321 (2026-09-15).
Data as JSON: /api/errors/f8a3651b9c5fa476.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/sync.service.ts:126
return this.syncCheckpointRepository.getAll(sessionId);
}
async setAcks(auth: AuthDto, dto: SyncAckSetDto) {
const sessionId = auth.session?.id;
if (!sessionId) {
return throwSessionRequired();
}
const checkpoints: Record<string, Insertable<SessionSyncCheckpointTable>> = {};
for (const ack of dto.acks) {
const { type } = fromAck(ack);
if (type === SyncEntityType.SyncResetV1) {
await this.sessionRepository.resetSyncProgress(sessionId);
return;
}
// TODO proper ack validation via class validator
if (!Object.values(SyncEntityType).includes(type)) {
throw new BadRequestException(`Invalid ack type: ${type}`);
}
// TODO pick the latest ack for each type, instead of using the last one
checkpoints[type] = { sessionId, type, ack };
}
await this.syncCheckpointRepository.upsertAll(Object.values(checkpoints));
}
async deleteAcks(auth: AuthDto, dto: SyncAckDeleteDto) {
const sessionId = auth.session?.id;
if (!sessionId) {
return throwSessionRequired();
}
await this.syncCheckpointRepository.deleteAll(sessionId, dto.types);
}
View on GitHub (pinned to f48d4b3321)