feat: consumeRouteRateLimit takes the array spec form too

Review follow-up on #3870: a multi-window spec passed whole would have
read an undefined window and silently never pruned. Charge each window
in order instead, refusing on the first refusal, matching the gate.
This commit is contained in:
Juan Castro committed 2026-09-15 18:55:05 -04:00
1 parent 1e0721fdc5
commit 57857cbb14
2 files changed
+21

No files matched your search

@@ -722,8 +722,15 @@ export async function checkRateLimit(key, limit, windowMs, backend) {
* budget. This resolves the key and the per-subscription limit exactly as the
* gate does — same spec in, same bucket out — so a handler can charge a second
* scope conditionally. Returns true if allowed; fails open on backend error.
* Takes the array form too; windows charge in order, no refund on refusal.
*/
export async function consumeRouteRateLimit(req, spec) {
if (Array.isArray(spec)) {
for (const window of spec) {
if (!(await consumeRouteRateLimit(req, window))) return false;
}
return true;
}
const {
window: windowMs,
key: strategy = 'fingerprint',
@@ -1506,6 +1506,20 @@ describe('consumeRouteRateLimit', () => {
expect(await consumeRouteRateLimit(reqFor(2), opts)).toBe(true);
});
it('charges every window of an array spec, and any refusal refuses', async () => {
// The array form routes use (burst + sustained); the tightest bites.
const specs = [
{ limit: 5, window: 60_000, key: 'user', scope: uniqueScope() },
{ limit: 2, window: 60_000, key: 'user', scope: uniqueScope() },
];
const req = { actor: { user: { id: 9 } }, headers: {} };
expect(await consumeRouteRateLimit(req, specs)).toBe(true);
expect(await consumeRouteRateLimit(req, specs)).toBe(true);
expect(await consumeRouteRateLimit(req, specs)).toBe(false);
// The refusing window really was the second one, not the first.
expect(await consumeRouteRateLimit(req, specs[0])).toBe(true);
});
it('applies bySubscription overrides via the wired metering service', async () => {
configureRateLimit({
metering: {