{"record":{"id":"8ef3742e10c24b4c","repo":"fullstackhero/dotnet-starter-kit","slug":"image-command-imageid-not-found-on-product-command-productid","errorCode":null,"errorMessage":"Image {command.ImageId} not found on product {command.ProductId}.","messagePattern":"Image (.+?) not found on product (.+?)\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Catalog/Modules.Catalog/Features/v1/Products/RemoveProductImage/RemoveProductImageCommandHandler.cs","lineNumber":24,"sourceCode":"\nnamespace FSH.Modules.Catalog.Features.v1.Products.RemoveProductImage;\n\npublic sealed class RemoveProductImageCommandHandler(CatalogDbContext dbContext)\n    : ICommandHandler<RemoveProductImageCommand, Unit>\n{\n    public async ValueTask<Unit> Handle(RemoveProductImageCommand command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        var product = await dbContext.Products\n            .FirstOrDefaultAsync(p => p.Id == command.ProductId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Product {command.ProductId} not found.\");\n\n        // Domain throws InvalidOperationException for unknown imageId; translate to 404.\n        if (!product.Images.Any(i => i.Id == command.ImageId))\n        {\n            throw new NotFoundException($\"Image {command.ImageId} not found on product {command.ProductId}.\");\n        }\n\n        product.RemoveImage(command.ImageId);\n        await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        return Unit.Value;\n    }\n}\n","sourceCodeStart":6,"sourceCodeEnd":32,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Catalog/Modules.Catalog/Features/v1/Products/RemoveProductImage/RemoveProductImageCommandHandler.cs#L6-L32","documentation":"RemoveProductImageCommandHandler verifies the requested ImageId exists in the product's Images collection before calling the domain; if not, it throws NotFoundException (HTTP 404). The comment in source notes the domain would otherwise throw an untranslated InvalidOperationException, so this check maps the miss to a proper 404.","triggerScenarios":"Removing an image id that is not part of the given product's image collection — already-removed image, image id belonging to a different product, or a client-side stale image list.","commonSituations":"Two operators removing the same image concurrently (second gets 404); UI cache of images not refreshed after a prior removal; copying image ids between products.","solutions":["Re-fetch the product's images and confirm the image id is still present before calling remove.","Treat 404 'image not found' as idempotent success in the UI — the goal state (image gone) already holds.","Refresh the image gallery after every removal so stale ids aren't reused.","Verify the image id belongs to the same product you are targeting."],"exampleFix":"// before\nawait api.removeProductImage(productId, imageId); // may 404\n// after\ntry {\n  await api.removeProductImage(productId, imageId);\n} catch (e) {\n  if (e.status === 404) return; // already removed — idempotent\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"const product = await api.getProduct(productId);\nif (!product.images.some(i => i.id === imageId)) return; // already gone","typeGuard":null,"tryCatchPattern":"try {\n  await api.removeProductImage(productId, imageId);\n} catch (e) {\n  if (e.status === 404) return; // idempotent: image already removed\n  throw e;\n}","preventionTips":["Treat image removal as idempotent — 404 means goal already achieved","Update the gallery optimistically and refetch to reconcile","Avoid queuing duplicate remove calls for the same image id"],"tags":["not-found","images","idempotency","http-404","catalog"],"backgroundTag":"entity-not-found","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}