immich-app/immich · error · BadRequestException

Crop parameters are out of bounds

Error message

Crop parameters are out of bounds

What it means

After dimensions are known, editAsset checks the crop rectangle against them: x+width must not exceed assetWidth and y+height must not exceed assetHeight. Violating this means the requested crop extends beyond the actual image, so the 400 is thrown and no edits are saved.

Solutions

  1. Clamp the crop rectangle to asset dimensions before sending: width = Math.min(width, assetWidth - x), same for height.
  2. Use the asset's original width/height (exifInfo.exifImageWidth/Height), not the preview/thumbnail size, to compute coordinates.
  3. Refresh the asset from the server before rendering the crop UI to avoid stale coordinates.
  4. Scale stored crop coordinates if the source image resolution changed.

Example fix

// before
await api.editAsset(assetId, { edits: [{ action: 'crop', parameters: { x, y, width, height } }] });
// after
const w = Math.min(width, asset.exifInfo.exifImageWidth - x);
const h = Math.min(height, asset.exifInfo.exifImageHeight - y);
await api.editAsset(assetId, { edits: [{ action: 'crop', parameters: { x, y, width: w, height: h } }] });
Defensive patterns

Strategy: validation

Validate before calling

const { exifImageWidth: aw, exifImageHeight: ah } = asset.exifInfo;
if (x < 0 || y < 0 || x + width > aw || y + height > ah) {
  throw new Error('Crop rectangle exceeds asset bounds');
}

Try / catch

try { await api.editAsset(id, { edits }); } catch (e) { if (e.status === 400 && /out of bounds/.test(e.message)) clampAndRetry(crop, asset); }

Prevention

When it happens

Trigger: PUT /api/assets/:id (editAsset) with a crop whose parameters satisfy x+width > assetWidth or y+height > assetHeight — e.g. cropping the original-size preview of a downscaled thumbnail, or stale coordinates from before the asset was replaced.

Common situations: Client using thumbnail dimensions instead of original dimensions for crop math; coordinates captured against a rotated/preview render; assets replaced with a different resolution while the client kept an old crop UI state.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15). Data as JSON: /api/errors/7c0aa5f328a0e063. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/asset.service.ts:583

    }

    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 } });

    // Return the asset and its applied edits
    return {
      assetId: id,
      edits: newEdits,
    };
  }

  async removeAssetEdits(auth: AuthDto, id: string): Promise<void> {
    await this.requireAccess({ auth, permission: Permission.AssetEditDelete, ids: [id] });

    const asset = await this.assetRepository.getById(id);
    if (!asset) {

View on GitHub (pinned to e55ac299a4)