Version: 0.1
Date: 2026-06-02
Type: Decision support. No code change. Recommendation only — operator decides.
Repo: DUNIN7/loomworks-engine, main. Upload endpoint operator_upload in src/loomworks/api/routers/uploads.py.
Question: When the object store (MinIO/S3) is unavailable, the upload endpoint returns a bare HTTP 500 (the v0.2 diagnosis root cause). Should it return 503 instead — consistent with the existing 503 the same file already returns for an in-handler put_blob failure?
Line refs re-verified against live code 2026-06-02 — no drift (see §1).
| Element | Location | Verified |
|---|---|---|
| Object-store dependency | deps.py:68 async def get_object_store(); :84 return build_object_store() | ✅ |
| In-dependency failure raise | build_object_store() → MinIOObjectStore.__init__ (minio_backend.py:53) → _ensure_bucket() → raise ObjectStoreError (minio_backend.py:60) | ✅ |
| In-handler 500 except | uploads.py:419 except Exception → :421 HTTP_500_INTERNAL_SERVER_ERROR, :422 detail "Executor failed unexpectedly: {exc}" (wraps execute_upload) | ✅ |
| In-handler 503 precedent | uploads.py:442 except ObjectStoreError → :444 HTTP_503_SERVICE_UNAVAILABLE, :445 detail "ObjectStore put_blob failed: {exc}" | ✅ |
| Per-file loop | uploads.py:379 for upload_file in files:; :542 per_file_responses.append(...); 200 response assembled :566–576 after the loop | ✅ |
| Dependency-raises-503 precedent | deps.py:87 get_secret_key already raise HTTPException(503) (:96–97) when a config dependency is unavailable | ✅ |
Mode A — object store down at request entry (the bare 500).
get_object_store is a FastAPI Depends. FastAPI resolves it in solve_dependencies before the handler body runs. build_object_store() constructs MinIOObjectStore, whose __init__ calls _ensure_bucket(), which calls MinIO (bucket_exists/make_bucket); if MinIO is unreachable it raises ObjectStoreError (minio_backend.py:60). Nothing catches it in dependency resolution, so FastAPI/Starlette emits its default 500 with body Internal Server Error and no detail. The handler's try/except blocks never execute.
Mode B — object store fails mid-handler (the 503).
If the store was reachable at entry but put_blob fails later (uploads.py:437), the in-handler except ObjectStoreError (:442) raises HTTPException(503, "ObjectStore put_blob failed: …").
Why the same underlying condition yields different codes. Both are the same failure type (ObjectStoreError = object store unavailable). The status differs **purely by where the error is raised relative to the handler body: in dependency resolution → uncaught → default 500; inside the handler → caught by the explicit except ObjectStoreError → 503**. It's an artifact of control-flow location, not a deliberate semantic distinction. A 503 ("service temporarily unavailable, retry later") is the correct representation for both — the 500 is just an unhandled leak.
get_object_store, raise 503 there
Wrap build_object_store() in deps.py:84 with try/except ObjectStoreError: raise HTTPException(503, …).
put_blob 503 (uploads.py:444) — same code for the same condition. Directly precedented in the same file: get_secret_key (deps.py:87) already raises 503 from a dependency when its backing config is unavailable, so a dependency raising 503 is an established pattern, not a new coupling. Fixes every endpoint that depends on get_object_store, not just uploads.get_secret_key, so minor). Only covers the construction/bucket-ensure failure path; a different object-store failure shape would need its own handling.ObjectStoreError → 503
Register @app.exception_handler(ObjectStoreError) returning 503.
put_blob except (uploads.py:442–445) be simplified/removed (DRY).ObjectStoreError raiser, some of which might warrant different treatment (e.g. a read/get_blob failure on a download path may deserve 404/410 rather than 503). Loses call-site-specific detail unless the handler reconstructs it. Larger review surface than the localized fix.
Defer build_object_store() from the dependency into the handler so its failure is caught by the existing handler try/except.
Depends-injection pattern that tests rely on (app.dependency_overrides[get_object_store] per deps.py docstring). Reduces testability for marginal benefit. Not recommended.No conflict. The tension is nominal, and the existing code already resolves it the same way.
The settled upload design returns HTTP 200 with a list of PerFileUploadResponse items, each carrying its own status (completed / detection_failed / transformation_failed / needs_clarification) — so per-file processing outcomes are reported in the body, not as HTTP status.
A request-level 503 for object-store outage operates at a different layer: it means "the request cannot be served at all," not "file N had outcome X." When the object store is down, no file can be persisted (every file needs put_blob), so there are no per-file outcomes to report — a request-level status is the only coherent representation.
Crucially, the existing code already treats object-store failure as request-level, overriding per-file reporting: put_blob's 503 is raised inside the per-file loop (uploads.py:379→:444), so a single file's store failure raises HTTPException and aborts the entire request, discarding any per_file_responses already accumulated. So:
Named edge (pre-existing, not introduced by any option): in a multi-file upload where the store is reachable at entry but fails after some files have been persisted, the current put_blob 503 abandons partial per-file results rather than reporting "files 1–2 ok, file 3 store-failed." That partial-success-then-store-failure representation gap already exists today and is independent of this decision. Flag it as a separate question if partial-batch fidelity ever matters; none of the three options here changes it.
Settled-commitment check: the memo's direction (request-level 503 for object-store outage) does not alter the 200 + per-file-status commitment — it concerns a failure mode where per-file processing cannot occur, and it conforms to the existing put_blob 503 precedent. No conflict found; nothing to flag in the seed (v0.9, which is silent on this CR-level detail) or CR-2026-092/093.
Recommend Option 1 (catch in get_object_store, raise 503), with a small regression test:
ObjectStoreError) produce the same code (503) regardless of where it surfaces; and it is doubly precedented in the existing codebase (uploads.py:444 for the same error type; deps.py:87 for a dependency raising 503).detail with a runbook hint (mirroring get_secret_key's style) so an operator seeing it knows to check the object-store service.ObjectStoreError consistency later becomes desirable (e.g. downloads, other writers), Option 2 is the natural consolidation — but it's a broader change better made deliberately, not folded into this fix.Explicitly deferred to you: whether to do Option 1 now, hold for Option 2's wider consolidation, or leave as-is. No code has been changed.