DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path investigations/object-store-outage-status-code-memo-v0_1.md

Object-store outage → status code: decision memo (v0.1)

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).


1. Verified current state (no drift)

| 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 | ✅ |


2. What actually happens now, per failure mode

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 ObjectStoreError503**. 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.


3. Options to make it consistent

Option 1 — Catch the failure in get_object_store, raise 503 there

Wrap build_object_store() in deps.py:84 with try/except ObjectStoreError: raise HTTPException(503, …).

Option 2 — App-level exception handler for ObjectStoreError → 503

Register @app.exception_handler(ObjectStoreError) returning 503.

Option 3 — Make object-store construction lazy / move it into the handler body

Defer build_object_store() from the dependency into the handler so its failure is caught by the existing handler try/except.


4. Does the settled 200 + per-file-status design create a conflict?

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.


5. Recommendation (for the operator to decide — not implemented)

Recommend Option 1 (catch in get_object_store, raise 503), with a small regression test:

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.