Skip to content

fix(api): answer 503 when the database node is paused, not a blanket 500 - #45

Merged
ralyodio merged 1 commit into
masterfrom
fix/db-paused-503
Aug 9, 2026
Merged

fix(api): answer 503 when the database node is paused, not a blanket 500#45
ralyodio merged 1 commit into
masterfrom
fix/db-paused-503

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

What was actually wrong

c0upons latest reported curl: (22) The requested URL returned error: 500. The API was not broken — the SQLite Cloud free-tier node is paused (errorCode: 10010), which I confirmed three ways: connecting directly with the prod SQLITECLOUD_URL, the SQLite Cloud HTTP API, and the Railway logs.

Error [SQLiteCloudError]: Your free node has been paused due to
inactivity. To resume usage, please restart your node from your
dashboard: https://dashboard.sqlitecloud.io      errorCode: '10010'

Restarting the node is still a manual dashboard action — this PR does not fix the outage. It fixes the fact that the outage was indistinguishable from a bug at every layer.

Changes

isDbPaused() (lib/db.ts) recognises the state — error code first, message as fallback. Deliberately not folded into isDisconnect(): a paused node refuses new sockets too, so the reconnect-and-retry path would pay the connection cost twice before failing identically.

dbErrorResponse() (lib/api-error.ts) maps it to 503 + Retry-After: 60 and a stable database_paused code. Every other failure keeps its existing 500 and message, so a genuine defect still reads as a defect. Applied to all 11 catch sites across the 8 non-webhook API routes.

CLI gains api_get(), mirroring the existing api_post(). Read commands now print the server's own sentence instead of a raw curl status, with distinct wording for 503 so people retry rather than reinstall. -L preserves the redirect-following the old curl -fsSL had.

before after
paused curl: (22) … error: 500 c0upons is temporarily unavailable. The coupon database is temporarily unavailable and should be back shortly.
real failure curl: (22) … error: 500 Error (500): Failed to fetch coupons

Drive-by: exit codes were inverted on success

Pre-existing, unrelated to the outage. [ "$i" -ge N ] && break was the last command in the search/latest loops, so on the final pass its status 1 became the script's exit code — a fully successful c0upons latest exited 1, meaning c0upons latest && echo ok never fired and CI would read success as failure. Verified on stock v1.1.0 before changing it.

Verification

Ran the app against the live paused node:

$ curl -i localhost/api/coupons
HTTP/1.1 503 Service Unavailable
retry-after: 60
{"error":"…temporarily unavailable…","code":"database_paused"}

/api/coupons, /api/stores, /api/search all 503. CLI prints the readable message. Against mock backends: success paths exit 0, 500 exits 1, 503 exits 1. tsc --noEmit clean; eslint shows only the 12 pre-existing errors in page.tsx/offline/privacy/terms — none in touched files.

CLI bumped to v1.2.0 (public/cli/c0upons is what install.sh and c0upons upgrade serve).

🤖 Generated with Claude Code

SQLite Cloud parks a free-tier node after a stretch of inactivity. Every
query then fails with error 10010, so `/api/coupons`, `/api/stores` and
`/api/search` all returned `{"error":"Failed to fetch …"}` with a 500 —
indistinguishable from a real defect, and the CLI rendered it as
`curl: (22) The requested URL returned error: 500`. Nothing in that chain
says "the database is down, wait a minute", so it reads as c0upons being
broken.

isDbPaused() recognises the state (error code first, message as a
fallback) and dbErrorResponse() maps it to 503 + Retry-After: 60 with a
stable `database_paused` code, leaving every other failure on its
existing 500 and message. It is deliberately kept out of isDisconnect():
a paused node refuses new sockets too, so the reconnect-and-retry path
would just pay the connection cost twice before failing identically.

The CLI grows an api_get() mirroring api_post(): read commands now print
the server's own sentence, with distinct wording for 503 so people retry
instead of reinstalling. -L preserves the redirect-following the old
`curl -fsSL` had.

Also fixes a pre-existing exit-code bug: `[ "$i" -ge N ] && break` was the
last command in the search/latest loops, so its status 1 on the final pass
became the script's exit code and a perfectly successful `c0upons latest`
reported failure to `&&` and CI.

Verified against the live paused node: 503 + database_paused on all three
read endpoints, CLI prints the readable message, and the success and 500
paths still exit 0 and 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

17 finding(s)

MEDIUM: 17

Severity Rule Location
MEDIUM sql-template-interpolation apps/web/app/api/bounties/[id]/claim/route.ts:18
MEDIUM sql-template-interpolation apps/web/app/api/bounties/[id]/claim/route.ts:34
MEDIUM sql-template-interpolation apps/web/app/api/bounties/[id]/claim/route.ts:43
MEDIUM sql-template-interpolation apps/web/app/api/bounties/[id]/claim/route.ts:73
MEDIUM sql-template-interpolation apps/web/app/api/bounties/route.ts:81
MEDIUM sql-template-interpolation apps/web/app/api/coupons/route.ts:68
MEDIUM sql-template-interpolation apps/web/app/api/coupons/route.ts:75
MEDIUM sql-template-interpolation apps/web/app/api/coupons/vote/route.ts:30
MEDIUM sql-template-interpolation apps/web/app/api/coupons/vote/route.ts:31
MEDIUM sql-template-interpolation apps/web/app/api/coupons/vote/route.ts:33
MEDIUM sql-template-interpolation apps/web/app/api/stores/[slug]/route.ts:9
MEDIUM sql-template-interpolation apps/web/app/api/webhooks/crawlproof/route.ts:83
MEDIUM sql-template-interpolation apps/web/app/api/webhooks/crawlproof/route.ts:90
MEDIUM js-open-redirect apps/web/app/bounties/new/page.tsx:57
MEDIUM js-unescaped-html-sink apps/web/app/layout.tsx:77
MEDIUM sql-template-interpolation apps/web/app/stores/[slug]/page.tsx:12
MEDIUM sql-template-interpolation apps/web/scripts/migrate.mjs:154

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 4dd4683 into master Aug 9, 2026
6 checks passed
@ralyodio
ralyodio deleted the fix/db-paused-503 branch August 9, 2026 12:38
ralyodio added a commit that referenced this pull request Aug 11, 2026
Submitting a coupon fails with "The coupon database is temporarily
unavailable" because the SQLite Cloud node is parked: every query, and
every new connection, answers error 10010 until someone restarts it from
the dashboard. The 503 mapping added in #45 reports that state correctly
— but nothing stops it happening, and it cannot be recovered in-process
because a parked node refuses new sockets too.

Prevention is the automatable half. /api/health/db runs a SELECT 1, and a
scheduled workflow calls it every 15 minutes, so the node never
accumulates enough idle time to be parked. The same endpoint doubles as
monitoring: an outage now fails a workflow run loudly instead of silently
emptying pages that swallow their own DB errors.

The schedule is best-effort by nature — GitHub delays scheduled runs
under load and disables them after 60 days of repo inactivity — so it
reduces the odds of a pause rather than eliminating them.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant