Context
The global rate-limit middleware (app.use("*", ...) in src/api/routes.ts, calling enforceRateLimit → rateLimitKey → rateLimitIdentity → installationRateLimitIdentity in src/auth/rate-limit.ts) runs before every route handler, including /v1/orb/token, /v1/orb/relay/register, and /v1/orb/relay/pull.
For those three paths, installationRateLimitIdentity calls validateOrbRelayEnrollment(c.env, token) unguarded:
if (INSTALLATION_KEYED_ORB_BEARER_PATHS.has(path)) {
const token = extractBearerToken(c.req.header("authorization"));
if (!token) return null;
const enrollment = await validateOrbRelayEnrollment(c.env, token);
return "error" in enrollment ? null : `installation:${enrollment.installationId}`;
}
If the DB throws inside validateOrbRelayEnrollment (a real, if rare, failure mode — D1/Postgres unavailable), the rejection propagates uncaught through this middleware, which has no try/catch around the await enforceRateLimit(...) call. Hono's default error handling then returns a bare framework 500, before the route handler is ever reached — so the route's own careful #4995 fix (.catch(dbBrokerError) around its own call to the same function, returning a clean 503 broker_error) never runs. The route-level fix only guards against the SECOND, later call to validateOrbRelayEnrollment; the rate-limit middleware's earlier call is a completely separate, unguarded call site.
Verified live by instrumenting /v1/orb/relay/register and /v1/orb/relay/pull's own existing #4995 regression tests (both currently pass on main because their assertion window is a distraction — the actual response is a bare 500, and the stack trace lands in installationRateLimitIdentity, not in the route handler):
Error: db unavailable
at Object.first (test/integration/orb-relay.test.ts:150:58)
at validateOrbRelayEnrollment (src/orb/relay.ts:224:6)
at installationRateLimitIdentity (src/auth/rate-limit.ts:240:24)
...
DEBUG STATUS 500 BODY Internal Server Error
The doc comment directly above INSTALLATION_KEYED_ORB_BEARER_PATHS already states the intended fallback behavior for exactly this situation — it just isn't implemented for the throw case: "prefer it when resolvable, falling back to IP-keying ... like every other route when it isn't -- a malformed payload, an unenrolled secret." A transient DB error is squarely another "not resolvable" case, and the sibling branch (peekWebhookInstallationId) already degrades this way via .catch(() => null).
This affects all three paths in INSTALLATION_KEYED_ORB_BEARER_PATHS: /v1/orb/token, /v1/orb/relay/register, /v1/orb/relay/pull.
Requirements
installationRateLimitIdentity's INSTALLATION_KEYED_ORB_BEARER_PATHS branch must never let a rejected validateOrbRelayEnrollment promise escape — degrade to null (falls back to IP-keying downstream in rateLimitIdentity), matching peekWebhookInstallationId's existing best-effort pattern.
- A regression test asserting the previously-red path: with the DB forced to throw inside
validateOrbRelayEnrollment's own query, a request to each of the three affected paths returns the route's OWN defined error response (the route-level 503 broker_error for the relay/token routes) — not a bare framework 500 — proving the middleware no longer intercepts the failure ahead of the handler.
- No change to the non-error resolution path: a valid/invalid/absent token must still resolve identity exactly as today.
Expected outcome
A DB hiccup during rate-limit identity resolution degrades to IP-keyed rate limiting for that one request, and the actual route handler (with its own well-tested error semantics) is what decides the response — never the rate-limit middleware.
Out of scope
Any change to peekWebhookInstallationId or the /v1/orb/relay single-tenant branch — both are unaffected by this bug.
Context
The global rate-limit middleware (
app.use("*", ...)insrc/api/routes.ts, callingenforceRateLimit→rateLimitKey→rateLimitIdentity→installationRateLimitIdentityinsrc/auth/rate-limit.ts) runs before every route handler, including/v1/orb/token,/v1/orb/relay/register, and/v1/orb/relay/pull.For those three paths,
installationRateLimitIdentitycallsvalidateOrbRelayEnrollment(c.env, token)unguarded:If the DB throws inside
validateOrbRelayEnrollment(a real, if rare, failure mode — D1/Postgres unavailable), the rejection propagates uncaught through this middleware, which has no try/catch around theawait enforceRateLimit(...)call. Hono's default error handling then returns a bare framework500, before the route handler is ever reached — so the route's own careful#4995fix (.catch(dbBrokerError)around its own call to the same function, returning a clean503 broker_error) never runs. The route-level fix only guards against the SECOND, later call tovalidateOrbRelayEnrollment; the rate-limit middleware's earlier call is a completely separate, unguarded call site.Verified live by instrumenting
/v1/orb/relay/registerand/v1/orb/relay/pull's own existing#4995regression tests (both currently pass onmainbecause their assertion window is a distraction — the actual response is a bare 500, and the stack trace lands ininstallationRateLimitIdentity, not in the route handler):The doc comment directly above
INSTALLATION_KEYED_ORB_BEARER_PATHSalready states the intended fallback behavior for exactly this situation — it just isn't implemented for the throw case: "prefer it when resolvable, falling back to IP-keying ... like every other route when it isn't -- a malformed payload, an unenrolled secret." A transient DB error is squarely another "not resolvable" case, and the sibling branch (peekWebhookInstallationId) already degrades this way via.catch(() => null).This affects all three paths in
INSTALLATION_KEYED_ORB_BEARER_PATHS:/v1/orb/token,/v1/orb/relay/register,/v1/orb/relay/pull.Requirements
installationRateLimitIdentity'sINSTALLATION_KEYED_ORB_BEARER_PATHSbranch must never let a rejectedvalidateOrbRelayEnrollmentpromise escape — degrade tonull(falls back to IP-keying downstream inrateLimitIdentity), matchingpeekWebhookInstallationId's existing best-effort pattern.validateOrbRelayEnrollment's own query, a request to each of the three affected paths returns the route's OWN defined error response (the route-level503 broker_errorfor the relay/token routes) — not a bare framework500— proving the middleware no longer intercepts the failure ahead of the handler.Expected outcome
A DB hiccup during rate-limit identity resolution degrades to IP-keyed rate limiting for that one request, and the actual route handler (with its own well-tested error semantics) is what decides the response — never the rate-limit middleware.
Out of scope
Any change to
peekWebhookInstallationIdor the/v1/orb/relaysingle-tenant branch — both are unaffected by this bug.