A FastAPI file-upload and thumbnail-processing API where the central engineering problem is not the upload itself but everything that has to happen correctly once the HTTP response has already been sent -- background image processing, a three-state lifecycle, and the precise status codes a polling client depends on.
Nobody has started this yet — be first.
Uploads exactly at the size cap are wrongly rejected
Every account on this service has a configured hard cap on upload size (IMAGESERVICE_MAX_UPLOAD_SIZE_BYTES, 10 MiB by default). A user whose file happens to be exactly at that limit -- not over it, exactly at it -- gets a 413 FILE_TOO_LARGE error today. From the user's point of view this looks like the file-size checker in the app is simply wrong: their file "is" the limit, and it's being told it exceeds the limit. This shows up as confusing support tickets ("your site says 10MB but rejects my 10MB file") that are hard for support staff to explain, because the stated limit and the actual enforced limit are silently off by one byte.
About this project
A FastAPI file-upload and image-processing API: it streams a multipart upload straight to disk in fixed-size chunks (never buffering the whole file in memory), sniffs the file's real content with Pillow rather than trusting its filename or Content-Type header, and generates three resized thumbnails (small/medium/large) from a single decode pass. Every filesystem operation goes through one seam -- StorageBackend / LocalDiskStorage in app/storage.py -- so nothing else in the app calls , , or directly, which is what makes a future S3-backed implementation a one-line swap instead of a rewrite.
Deleting an image leaks its thumbnail files on disk
DELETE /images/{id} returns a clean 204 No Content and the database row genuinely disappears -- so from the outside, deletion looks 100% successful. But the three thumbnail files it generated (thumb_small.*, thumb_medium.*, thumb_large.*) are never removed from disk. Every delete of a fully-processed image now leaks 3 files with nothing left in the database to ever reference or clean them up again. On a service whose whole job is storing images, this is a slow, silent disk-space leak that will eventually page whoever is on call when the disk fills up -- and by then there is no record left anywhere of which orphaned files belong to which (already-deleted) user upload, making cleanup a forensic exercise instead of a query.
open()PathshutilThe central engineering problem is that thumbnail generation is real CPU work scheduled via FastAPI's BackgroundTasks and run after the HTTP response has already gone back to the client -- so the API has to model an image's lifecycle as a small state machine (processing -> ready or failed) instead of pretending the work finished synchronously. That ripples into every corner of the surface: GET /images/{id} exposes status for polling, the thumbnail-download route has to give three distinct, well-defined answers for "not ready yet" (425 Too Early), "will never be ready" (409 Conflict), and "doesn't exist" (404), and a background task that never gets to run at all -- because the process crashed, deployed, or was OOM-killed between the record being created and the pipeline committing its result -- leaves a row stuck at "processing" forever unless something later notices, using updated_at as the staleness signal, and reaps it.
Seven tickets (4 bugs, 3 enhancements) are deliberately injected into an otherwise fully working, tested codebase: a couple are single-comparison-operator boundary bugs, one is a cross-file contract drift where two independently-maintained lists of "the three thumbnail sizes" quietly fall out of sync, one is a pair of swapped HTTP status-code branches that inverts a client's entire polling logic, and the rest are compile-clean stubs -- a pagination endpoint, a retry endpoint, and a stale-record reaper -- waiting for their real implementation. Every ticket has its own dedicated pytest file that goes green when the fix is correct, and several of them are real enough to also break specific tests in the project's own pre-existing tests/ suite, which is intentional: it's a second, independent confirmation that the bug is real.