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

  1. Reorder the edits array so the Crop action is element 0 before sending.
  2. Split into two requests: first submit non-crop edits, then submit the crop as the first action of the next replacement.
  3. 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

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


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)