fix(api): report partial failures and bound batch bodies on image routes - #9394
Open
lstein wants to merge 4 commits into
Open
fix(api): report partial failures and bound batch bodies on image routes#9394lstein wants to merge 4 commits into
lstein wants to merge 4 commits into
Conversation
Three related gaps on the image endpoints, all of which the video endpoints already handle (they were fixed there during the invoke-ai#9163 review): 1. `star_images_in_list` / `unstar_images_in_list` re-raised the first HTTPException mid-batch, so one foreign name discarded the response payload for images that HAD been starred — the client never invalidated their caches and the UI showed them unstarred until the next full refresh. They now skip foreign/missing names like `delete_images_from_list` does, and dedup repeated names so one name can't land in two result buckets. 2. Those same handlers swallowed genuine storage failures with `except Exception: pass`, reporting a success-shaped response for images that were never updated. `StarredImagesResult` / `UnstarredImagesResult` gain `failed_images` (mirroring `DeleteImagesResult` and the video models), and the frontend toasts a partial-failure warning like the video star/unstar mutations do. 3. The `image_names` batch bodies (delete/star/unstar/images_by_names) were unbounded, and `list_image_dtos` had no pagination bounds — a negative LIMIT means *unlimited* in SQLite. Adds MAX_IMAGE_BATCH_SIZE (mirroring MAX_VIDEO_BATCH_SIZE), a 255-char per-name cap, and ge=0/le=MAX_PAGE_SIZE on the list route. The lower bound on `limit` is 0, not 1: the frontend issues count-only queries with limit=0. Deferred non-blocker from PR invoke-ai#9163. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lstein
requested review from
JPPhoto,
Pfannkuchensack,
blessedcoolant and
dunkeroni
as code owners
July 28, 2026 01:39
…atch bounds Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JPPhoto
requested changes
Aug 9, 2026
JPPhoto
left a comment
Collaborator
There was a problem hiding this comment.
I came across the following issue:
invokeai/app/api/routers/images.py:695-727:/downloadstill accepts unboundedimage_names, performs per-name authorization, then schedules bulk work. An authenticated client can submit an oversized body and consume request/DB resources despite the new batch limits elsewhere. Test: POST 1001 names and assert 422 before authorization or background-task execution.
Suggestions:
- Consider a shared
ImageNamesBatchmodel for every explicit-name batch endpoint; this would apply limits consistently and prevent/downloaddrift.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-on to #9163 (deferred non-merge-blocker). The video endpoints got these three fixes during the review; the image endpoints they were modelled on never did, so images and videos in the same selection behave differently today.
1. Batch star/unstar aborted on the first foreign name
star_images_in_list/unstar_images_in_listdidexcept HTTPException: raiseinside the per-name loop. One name the caller doesn't own — or one deleted by a concurrent session — discarded the response payload for every image that had just been starred. The client then never invalidated those caches, so the UI showed them unstarred until a full refresh.They now skip such names, matching
delete_images_from_listand the video routes, and dedup repeated names so a name repeated in one request can't land in both result buckets.test_non_owner_cannot_star_imageis updated for the new response shape (200 + emptystarred_imagesinstead of 403) and now also asserts the underlying record is still unstarred — the authorization guarantee is unchanged, only the reporting is.2. Storage failures were silently reported as success
The same loops swallowed real failures with
except Exception: pass, so a star that never reached the DB came back looking applied and vanished on reload.StarredImagesResult/UnstarredImagesResultgain afailed_imageslist (mirroringDeleteImagesResultandStarredVideosResult), populated for genuine failures only — an auth skip is not a failure and must not be toasted as one. The frontend toasts the partial-failure warning the video mutations already show.3. Unbounded request bodies and pagination
image_nameson delete/star/unstar/images_by_names was unbounded; each name costs at least one DB lookup, so an authenticated client could pin a worker with one request. AddsMAX_IMAGE_BATCH_SIZE = 1000(mirroringMAX_VIDEO_BATCH_SIZE) plus a 255-char per-name cap.list_image_dtoshad no pagination bounds. A negativeLIMITmeans unlimited in SQLite, solimit=-1materialized every image row into a DTO. Addsge=0/le=MAX_PAGE_SIZE, matching the video list route. Lower bound is 0, not 1 — the frontend issueslimit=0count-only queries (useHasImages).Testing
pytest tests/app/routers/test_images.py— 32 passed (7 new).pytest tests/app/routers— 615 passed.lint:tsc,lint:eslint,lint:prettierclean;test:no-watch1554 passed.schema.tsregenerated with the locked toolchain.🤖 Generated with Claude Code