immich-app/immich · warning · BadRequestException
Crop action must be the first edit action
Error message
Crop action must be the first edit action
What it means
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.
Source
Thrown at server/src/services/asset.service.ts:566
throw new BadRequestException('Editing GIF images is not supported');
}
if (asset.originalPath?.toLowerCase().endsWith('.svg')) {
throw new BadRequestException('Editing SVG images is not supported');
}
// check that crop parameters will not go out of bounds
const { width: assetWidth, height: assetHeight } = getDimensions(asset);
if (!assetWidth || !assetHeight) {
throw new BadRequestException('Asset dimensions are not available for editing');
}
const edits = dto.edits as AssetEditActionItem[];
const crop = edits.find((e) => e.action === AssetEditAction.Crop);
if (crop) {
if (edits[0].action !== AssetEditAction.Crop) {
throw new BadRequestException('Crop action must be the first edit action');
}
// check that crop parameters will not go out of bounds
const { width: assetWidth, height: assetHeight } = getDimensions(asset);
if (!assetWidth || !assetHeight) {
throw new BadRequestException('Asset dimensions are not available for editing');
}
const { x, y, width, height } = crop.parameters;
if (x + width > assetWidth || y + height > assetHeight) {
throw new BadRequestException('Crop parameters are out of bounds');
}
}
const newEdits = await this.assetEditRepository.replaceAll(id, edits);
await this.jobRepository.queue({ name: JobName.AssetEditThumbnailGeneration, data: { id } });
View on GitHub (pinned to 199723261c)
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'.
Example fix
// before const edits = [...rotateEdits, cropEdit]; // after const edits = [...(cropEdit ? [cropEdit] : []), ...rotateEdits, ...mirrorEdits];
Defensive patterns
Strategy: validation
Validate before calling
// Enforce crop-first ordering before submit.
const edits = [...payload.edits];
const cropIdx = edits.findIndex((e) => e.action === 'Crop');
if (cropIdx > 0) {
const [crop] = edits.splice(cropIdx, 1);
edits.unshift(crop);
}
if (cropIdx !== -1 && edits[0].action !== 'Crop') {
throw new Error('Crop must be first');
}
await api.put(`/assets/${id}/edits`, { edits }); Prevention
- 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.
When it happens
Trigger: PUT /assets/{id}/edits with a body like [{action:'Rotate',...},{action:'Crop',...}] — crop present but not at index 0.
Common situations: 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.
Related errors
- Crop parameters are out of bounds
- Only images can be edited
- Editing live photos is not supported
- Editing panorama images is not supported
- Editing GIF images is not supported
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/88170c04acb271d6.
Report an issue: GitHub.