{"record":{"id":"f8a3651b9c5fa476","repo":"immich-app/immich","slug":"invalid-ack-type-type","errorCode":null,"errorMessage":"Invalid ack type: ${type}","messagePattern":"Invalid ack type: (.+?)","errorType":"validation","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/sync.service.ts","lineNumber":126,"sourceCode":"    return this.syncCheckpointRepository.getAll(sessionId);\n  }\n\n  async setAcks(auth: AuthDto, dto: SyncAckSetDto) {\n    const sessionId = auth.session?.id;\n    if (!sessionId) {\n      return throwSessionRequired();\n    }\n\n    const checkpoints: Record<string, Insertable<SessionSyncCheckpointTable>> = {};\n    for (const ack of dto.acks) {\n      const { type } = fromAck(ack);\n      if (type === SyncEntityType.SyncResetV1) {\n        await this.sessionRepository.resetSyncProgress(sessionId);\n        return;\n      }\n      // TODO proper ack validation via class validator\n      if (!Object.values(SyncEntityType).includes(type)) {\n        throw new BadRequestException(`Invalid ack type: ${type}`);\n      }\n\n      // TODO pick the latest ack for each type, instead of using the last one\n      checkpoints[type] = { sessionId, type, ack };\n    }\n\n    await this.syncCheckpointRepository.upsertAll(Object.values(checkpoints));\n  }\n\n  async deleteAcks(auth: AuthDto, dto: SyncAckDeleteDto) {\n    const sessionId = auth.session?.id;\n    if (!sessionId) {\n      return throwSessionRequired();\n    }\n\n    await this.syncCheckpointRepository.deleteAll(sessionId, dto.types);\n  }\n","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/immich-app/immich/blob/f48d4b332127ad365ba256108799ca8f571d2dd5/server/src/services/sync.service.ts#L108-L144","documentation":"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.","triggerScenarios":"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.","commonSituations":"Version mismatch between mobile client and server after downgrade; hand-crafted API requests; corrupted local ack database sending stale/invalid types.","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"],"exampleFix":"// before\nawait setAcks([{ type: 'AssetV1', ack: '...' }])\n// after\nawait setAcks([{ type: SyncEntityType.AssetV1, ack: '...' }])","handlingStrategy":"validation","validationCode":"const validTypes = Object.values(SyncEntityType);\nconst isValidAck = (a) => validTypes.includes(a.type);\nacks.forEach(a => { if (!isValidAck(a)) throw new Error(`Invalid ack type: ${a.type}`); });","typeGuard":"const isValidSyncEntityType = (t) => Object.values(SyncEntityType).includes(t);","tryCatchPattern":"try {\n  await syncClient.setAcks(acks);\n} catch (e) {\n  if (e.status === 400 && e.message.startsWith('Invalid ack type')) {\n    acks = acks.filter(a => isValidSyncEntityType(a.type)); // drop stale types\n  }\n}","preventionTips":["Filter client acks against the server's SyncEntityType enum before sending","Keep client and server versions aligned","Reset sync state after server downgrades"],"tags":["sync","validation","enum","bad-request"],"backgroundTag":"invalid-enum-value","analyzedSha":"f48d4b332127ad365ba256108799ca8f571d2dd5","analyzedAt":"2026-09-15T07:20:19.675Z","contentChangedAt":"2026-09-15T07:20:19.675Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}