{"record":{"id":"7c0aa5f328a0e063","repo":"immich-app/immich","slug":"crop-parameters-are-out-of-bounds","errorCode":null,"errorMessage":"Crop parameters are out of bounds","messagePattern":"Crop parameters are out of bounds","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","filePath":"server/src/services/asset.service.ts","lineNumber":578,"sourceCode":"    }\n\n    const edits = dto.edits as AssetEditActionItem[];\n    const crop = edits.find((e) => e.action === AssetEditAction.Crop);\n    if (crop) {\n      if (edits[0].action !== AssetEditAction.Crop) {\n        throw new BadRequestException('Crop action must be the first edit action');\n      }\n\n      // check that crop parameters will not go out of bounds\n      const { width: assetWidth, height: assetHeight } = getDimensions(asset);\n\n      if (!assetWidth || !assetHeight) {\n        throw new BadRequestException('Asset dimensions are not available for editing');\n      }\n\n      const { x, y, width, height } = crop.parameters;\n      if (x + width > assetWidth || y + height > assetHeight) {\n        throw new BadRequestException('Crop parameters are out of bounds');\n      }\n    }\n\n    const newEdits = await this.assetEditRepository.replaceAll(id, edits);\n    await this.jobRepository.queue({ name: JobName.AssetEditThumbnailGeneration, data: { id } });\n\n    // Return the asset and its applied edits\n    return {\n      assetId: id,\n      edits: newEdits,\n    };\n  }\n\n  async removeAssetEdits(auth: AuthDto, id: string): Promise<void> {\n    await this.requireAccess({ auth, permission: Permission.AssetEditDelete, ids: [id] });\n\n    const asset = await this.assetRepository.getById(id);\n    if (!asset) {","sourceCodeStart":560,"sourceCodeEnd":596,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/asset.service.ts#L560-L596","documentation":"Thrown by editAsset inside the crop block when the requested crop rectangle exceeds the asset bounds: crop.parameters.x + width > assetWidth OR y + height > assetHeight. The server validates geometry against the asset's real dimensions and rejects oversized/off-edge rectangles with 400 BadRequest.","triggerScenarios":"PUT /assets/{id}/edits with a Crop whose x+width or y+height exceeds the asset's pixel width/height; crop rectangle built against a downscaled preview but submitted against full-resolution dimensions; negative x/y making the rect extend past the edge.","commonSituations":"Client computes the crop on a preview image but sends coordinates in a different scale than the server's stored dimensions; rounding errors that push width/height one pixel over; stale asset dimensions after a re-encode.","solutions":["Clamp the crop rectangle to [0,0,assetWidth,assetHeight] before submitting.","Compute crop coordinates from the same width/height the server reports (asset.exifInfo width/height).","Subtract a 1px safety margin to avoid off-by-one overshoot."],"exampleFix":"// before\nconst crop = { x, y, width, height };\n\n// after\nconst x = Math.max(0, Math.min(crop.x, assetWidth - 1));\nconst y = Math.max(0, Math.min(crop.y, assetHeight - 1));\nconst width = Math.min(crop.width, assetWidth - x);\nconst height = Math.min(crop.height, assetHeight - y);\nconst crop = { x, y, width, height };","handlingStrategy":"validation","validationCode":"// Clamp the crop rect to the asset's real dimensions before submit.\nconst assetWidth = asset.exifInfo?.width;\nconst assetHeight = asset.exifInfo?.height;\nconst x = Math.max(0, Math.min(crop.x, assetWidth - 1));\nconst y = Math.max(0, Math.min(crop.y, assetHeight - 1));\nconst width = Math.min(crop.width, assetWidth - x);\nconst height = Math.min(crop.height, assetHeight - y);\nif (x + width > assetWidth || y + height > assetHeight) {\n  throw new Error('Crop rect still out of bounds after clamping');\n}\nawait api.put(`/assets/${id}/edits`, { edits: [{ action: 'Crop', parameters: { x, y, width, height } }, ...rest] });","typeGuard":null,"tryCatchPattern":"try {\n  await api.put(`/assets/${id}/edits`, { edits });\n} catch (e) {\n  if (e.response?.status === 400 && /out of bounds/i.test(e.response?.data?.message)) {\n    edits = clampCropToAsset(edits, asset);\n    await api.put(`/assets/${id}/edits`, { edits });\n  } else throw e;\n}","preventionTips":["Compute crop coordinates in the same coordinate space the server uses (full-res dimensions).","Clamp x+width <= assetWidth and y+height <= assetHeight before submitting.","Leave a 1px safety margin to absorb rounding."],"tags":["asset","edit","crop","geometry","validation","bounds"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}