immich-app/immich · error · BadRequestException
Crop action must be the first edit action
Error message
Crop action must be the first edit action
What it means
editAsset enforces ordering: if the edits array contains a Crop action, it must be the first element. Other actions (rotate, flip) change the coordinate space, so a crop that follows them would have ambiguous semantics; Immich rejects any crop that is not edits[0] with this 400.
Solutions
- Reorder the edits array so the Crop action is element 0 before sending.
- Split into two requests: first submit non-crop edits, then submit the crop as the first action of the next replacement.
- Alternatively transform the crop coordinates client-side into the rotated space and drop the separate rotate action.
Example fix
// before
edits = [{ action: 'rotate', parameters: { rotation: 90 } }, { action: 'crop', parameters: { x: 10, y: 10, width: 100, height: 100 } }];
// after
edits = [{ action: 'crop', parameters: { x: 10, y: 10, width: 100, height: 100 } }, { action: 'rotate', parameters: { rotation: 90 } }]; Defensive patterns
Strategy: validation
Validate before calling
const cropIdx = edits.findIndex(e => e.action === 'crop');
if (cropIdx > 0) throw new Error('Crop must be the first edit action'); Try / catch
try { await api.editAsset(id, { edits }); } catch (e) { if (e.status === 400) edits = reorderCropFirst(edits) && retry; } Prevention
- Always serialize crop before rotate/flip when building edit lists
- Centralize edit-list construction in one helper that enforces ordering
- Document ordering constraints in your edit-pipeline code
When it happens
Trigger: PUT /api/assets/:id (editAsset) with dto.edits like [{action:'rotate'},{action:'crop',parameters:{...}}] — any edit list where a Crop action appears at index > 0.
Common situations: UI pipelines that batch operations in the order the user performed them (rotate then crop); merging accumulated edit history where a crop was appended after an earlier rotation; two clients appending edits to the same asset.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Asset dimensions are not available for editing
- Crop parameters are out of bounds
- User not found
- Invalid cursor
- Invalid license key
AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15).
Data as JSON: /api/errors/88170c04acb271d6.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/asset.service.ts:571
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 e55ac299a4)