Database outage renders as 500 internal_error, not the retryable 503, on /ipfs/pins, /ipfs/{cid} and /arweave/anchors #251
Copy link
Copy link
Closed
Labels
crate:nodegitlawb-node — the serving node and REST APIgitlawb-node — the serving node and REST APIkind:bugDefect fix — wrong or unsafe behaviorDefect fix — wrong or unsafe behaviorsev:lowCosmetic, cleanup, or nice-to-haveCosmetic, cleanup, or nice-to-havesubsystem:apiNode REST API request/response surfaceNode REST API request/response surface
Description
Activity
- addedsev:lowCosmetic, cleanup, or nice-to-haveCosmetic, cleanup, or nice-to-havecrate:nodegitlawb-node — the serving node and REST APIgitlawb-node — the serving node and REST APIkind:bugDefect fix — wrong or unsafe behaviorDefect fix — wrong or unsafe behaviorsubsystem:apiNode REST API request/response surfaceNode REST API request/response surface
on Jul 27, 2026 @beardthelion — fix for this is in #254
Replaced
.map_err(AppError::Internal)with bare?on the ipfs/arweave DB call sites so connection-class failures downcast toAppError::Db→ 503db_unavailable. Closed-pool route tests + a downcast unit test included.- added 4 commits that reference this issue
on Aug 10, 2026 - added a commit that references this issue
on Aug 11, 2026 - added a commit that references this issue
on Aug 11, 2026
Metadata
Metadata
Assignees
Labels
crate:nodegitlawb-node — the serving node and REST APIgitlawb-node — the serving node and REST APIkind:bugDefect fix — wrong or unsafe behaviorDefect fix — wrong or unsafe behaviorsev:lowCosmetic, cleanup, or nice-to-haveCosmetic, cleanup, or nice-to-havesubsystem:apiNode REST API request/response surfaceNode REST API request/response surface
What happens
On
main(111cff7), when the database is unreachable, three routes return500 internal_errorwhere every sibling route returns503 db_unavailable. Observed through the real mounted router against a closed pool, all four requests anonymous and unsigned:Same process, same pool, same request cycle. The split is per handler, not a deliberate policy.
Why
The db layer returns
anyhow::Result, andimpl From<anyhow::Error> for AppErrorincrates/gitlawb-node/src/error.rsis the only place that downcasts asqlx::Errorback out of the anyhow chain intoAppError::Db.IntoResponsethen sendsAppError::Db(e)throughdb_unavailable(e), which maps the connection-level variants (PoolTimedOut,PoolClosed,Io,Tls) to 503.A handler that writes
.map_err(AppError::Internal)rather than a bare?constructs theInternalvariant directly, so that downcast never runs and the error takes the unconditional 500 arm.Sites on main:
crates/gitlawb-node/src/api/ipfs.rs:84crates/gitlawb-node/src/api/ipfs.rs:94crates/gitlawb-node/src/api/ipfs.rs:224crates/gitlawb-node/src/api/arweave.rs:33Why it matters
503 is the code that means "temporary, retry", and 500 means "this request is broken". A proxy, an uptime check, or a client retry policy keyed on 503 will not fire for these routes during a database outage, so a recoverable condition reads as a hard failure on exactly the metadata endpoints most likely to be polled. All three routes are unauthenticated (
/api/v1/ipfs/pinsand/api/v1/arweave/anchorscarry no auth layer,/ipfs/{cid}carries onlyoptional_signature), so the behavior is observable by anyone.Scope
Pre-existing, and not introduced by #247. That PR makes the body of both 500 arms opaque, which is a separate and correct change; the status split survives it, which is why this is its own issue rather than a review comment there.
#134 is currently in flight on
api/ipfs.rsandapi/arweave.rs. It rewrites the arweave call site and adds three more.map_err(AppError::Internal)?sites, so a fix here should land after it or be coordinated with it, and should cover the sites #134 adds rather than only the four above.Fix direction
Use a bare
?at these call sites so the existingFrom<anyhow::Error>conversion runs and connection-class failures reach the 503 arm. Non-sqlx errors still land inInternalthrough that same conversion, so nothing else changes shape. I have not compiled that change, so this is the direction rather than a verified patch.A regression test would drive one of these routes against a closed pool and assert 503 plus
db_unavailable, which is the shape the reproduction above already uses: build the router from a migrated test pool, callpool.close(), then issue the request.