Skip to content

fix: fail closed when rate limiter bindings are missing or erroring - #48

Merged
DavidHDev merged 1 commit into
DavidHDev:mainfrom
bilalmlkdev:fix/rate-limiter-fail-closed
Nov 8, 2026
Merged

DavidHDev merged 1 commit into
DavidHDev:mainfrom
bilalmlkdev:fix/rate-limiter-fail-closed

Conversation

@bilalmlkdev

Copy link
Copy Markdown
Contributor

What does this PR do?

The allowed() helper in worker/index.ts silently failed open: if the
SUBSCRIBE_IP_LIMIT / SUBSCRIBE_GLOBAL_LIMIT bindings were missing or the
limit() call threw, every request was treated as within limits, leaving the
/api/subscribe endpoint (and the Resend API quota behind it) unprotected with
no trace in the logs.

This PR replaces allowed() with a checkLimit() helper that returns a
tri-state result:

  • "ok" - within limits, request proceeds (unchanged)
  • "limited" - over the limit, returns 429 (unchanged)
  • "error" - binding missing or limiter threw: logs the failure with
    console.error (naming the offending binding) and returns 503
    "Signups are unavailable right now.", consistent with how other missing
    configuration (e.g. RESEND_API_KEY, TURNSTILE_SECRET_KEY) is handled

Misconfiguration now surfaces loudly in wrangler tail instead of failing
silently, and a limiter outage degrades to "signups temporarily unavailable"
rather than "no rate limiting at all".

Type of change

  • Bug fix
  • New component
  • Component improvement
  • Docs / site
  • Other

Checklist

  • npx tsc --noEmit passes
  • npm run lint passes
  • npm run build passes
  • Tested in Chrome (with the html-in-canvas flag, if relevant)
  • Tested the WebGL overlay fallback (flag off / Safari / Firefox), if a component changed

Note: the change is worker-only (no components touched). I was unable to run
the checks locally due to network issues installing dependencies - relying on
CI; happy to follow up if anything fails.

@bilalmlkdev

Copy link
Copy Markdown
Contributor Author

Hey David, circling back on this one when you get a chance.

The only real judgment call here is what /subscribe should do when the
limiter itself is unavailable. Right now allowed() returns true both when
the binding is missing and when limit() throws, so a limiter outage
quietly removes the abuse protection. This changes that to a 503, which
matches how the endpoint already treats missing config a few lines above,
RESEND_API_KEY and TURNSTILE_SECRET_KEY both return 503 UNAVAILABLE.

The trade-off is availability: a limiter hiccup would block real signups
instead of letting a few extra through. If you'd rather stay up in that
case, I can narrow it, fail closed when the binding is absent (so a bad
deploy can't silently disable limits) but keep the catch path failing
open with a log line. That's a one-line change either way.

No rush, I just didn't want to leave the decision sitting unanswered.

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.

2 participants