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
- Clamp the crop rectangle to asset dimensions before sending: width = Math.min(width, assetWidth - x), same for height.
- Use the asset's original width/height (exifInfo.exifImageWidth/Height), not the preview/thumbnail size, to compute coordinates.
- Refresh the asset from the server before rendering the crop UI to avoid stale coordinates.
- 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
- Compute crop coordinates against original dimensions, not thumbnails/previews
- Clamp rectangles client-side before submit
- Refresh asset state before rendering crop UI to avoid stale coordinates
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
- Asset dimensions are not available for editing
- Crop action must be the first edit action
- User not found
- Invalid cursor
- Invalid license key
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)