{"record":{"id":"88170c04acb271d6","repo":"immich-app/immich","slug":"crop-action-must-be-the-first-edit-action","errorCode":null,"errorMessage":"Crop action must be the first edit action","messagePattern":"Crop action must be the first edit action","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","filePath":"server/src/services/asset.service.ts","lineNumber":566,"sourceCode":"      throw new BadRequestException('Editing GIF images is not supported');\n    }\n\n    if (asset.originalPath?.toLowerCase().endsWith('.svg')) {\n      throw new BadRequestException('Editing SVG images is not supported');\n    }\n\n    // check that crop parameters will not go out of bounds\n    const { width: assetWidth, height: assetHeight } = getDimensions(asset);\n\n    if (!assetWidth || !assetHeight) {\n      throw new BadRequestException('Asset dimensions are not available for editing');\n    }\n\n    const edits = dto.edits as AssetEditActionItem[];\n    const crop = edits.find((e) => e.action === AssetEditAction.Crop);\n    if (crop) {\n      if (edits[0].action !== AssetEditAction.Crop) {\n        throw new BadRequestException('Crop action must be the first edit action');\n      }\n\n      // check that crop parameters will not go out of bounds\n      const { width: assetWidth, height: assetHeight } = getDimensions(asset);\n\n      if (!assetWidth || !assetHeight) {\n        throw new BadRequestException('Asset dimensions are not available for editing');\n      }\n\n      const { x, y, width, height } = crop.parameters;\n      if (x + width > assetWidth || y + height > assetHeight) {\n        throw new BadRequestException('Crop parameters are out of bounds');\n      }\n    }\n\n    const newEdits = await this.assetEditRepository.replaceAll(id, edits);\n    await this.jobRepository.queue({ name: JobName.AssetEditThumbnailGeneration, data: { id } });\n","sourceCodeStart":548,"sourceCodeEnd":584,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/asset.service.ts#L548-L584","documentation":"Thrown by editAsset inside the crop block: when the edits array contains an AssetEditAction.Crop but it is not the first element (edits[0].action !== Crop), the service rejects with 400 BadRequest. Crop must lead the sequence because subsequent rotate/mirror operations are applied in the cropped coordinate space.","triggerScenarios":"PUT /assets/{id}/edits with a body like [{action:'Rotate',...},{action:'Crop',...}] — crop present but not at index 0.","commonSituations":"Client appends crop after rotate when building the edit stack; user applies rotate first then crops in the UI but the client emits actions in interaction order rather than required order.","solutions":["When building the edits payload, sort so any Crop action is index 0.","If the UI lets the user rotate then crop, emit [{Crop},{Rotate}] on submit.","Validate the payload client-side: if any edit is Crop, assert edits[0].action === 'Crop'."],"exampleFix":"// before\nconst edits = [...rotateEdits, cropEdit];\n\n// after\nconst edits = [...(cropEdit ? [cropEdit] : []), ...rotateEdits, ...mirrorEdits];","handlingStrategy":"validation","validationCode":"// Enforce crop-first ordering before submit.\nconst edits = [...payload.edits];\nconst cropIdx = edits.findIndex((e) => e.action === 'Crop');\nif (cropIdx > 0) {\n  const [crop] = edits.splice(cropIdx, 1);\n  edits.unshift(crop);\n}\nif (cropIdx !== -1 && edits[0].action !== 'Crop') {\n  throw new Error('Crop must be first');\n}\nawait api.put(`/assets/${id}/edits`, { edits });","typeGuard":null,"tryCatchPattern":null,"preventionTips":["When the user rotates then crops, still emit Crop as the first action.","Add a client-side payload validator asserting edits[0].action === 'Crop' when a Crop is present.","Document the ordering contract for any client building edit payloads."],"tags":["asset","edit","crop","validation","ordering"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}