fix(dashboard): bump in-memory rate limits to avoid spurious 429s

The shared per-IP mutation bucket (30/min) made common dashboard actions
like Respecify fail with "Too many requests" after light activity. Raise
mutation to 600/min, api to 1000/min, sse to 60/min for the local-first
use case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-23 21:00:51 -07:00
parent 3a39ad66b2
commit b354e49940
3 changed files with 17 additions and 17 deletions

View File

@@ -29,15 +29,15 @@ function mockRes(): Partial<Response> & { _status: number; _json: any; _headers:
describe("RATE_LIMITS constants", () => {
it("has correct values for api limit", () => {
expect(RATE_LIMITS.api).toEqual({ windowMs: 60_000, max: 100 });
expect(RATE_LIMITS.api).toEqual({ windowMs: 60_000, max: 1000 });
});
it("has correct values for mutation limit", () => {
expect(RATE_LIMITS.mutation).toEqual({ windowMs: 60_000, max: 30 });
expect(RATE_LIMITS.mutation).toEqual({ windowMs: 60_000, max: 600 });
});
it("has correct values for sse limit", () => {
expect(RATE_LIMITS.sse).toEqual({ windowMs: 60_000, max: 10 });
expect(RATE_LIMITS.sse).toEqual({ windowMs: 60_000, max: 60 });
});
});
@@ -200,7 +200,7 @@ describe("rateLimit with RATE_LIMITS presets", () => {
apiMiddleware(req as Request, res as unknown as Response, () => { called = true; });
expect(called).toBe(true);
expect(res._headers["RateLimit-Limit"]).toBe("100");
expect(res._headers["RateLimit-Limit"]).toBe(String(RATE_LIMITS.api.max));
});
it("works with RATE_LIMITS.mutation preset", () => {
@@ -208,11 +208,11 @@ describe("rateLimit with RATE_LIMITS presets", () => {
const req = mockReq();
// Exhaust limit
for (let i = 0; i < 30; i++) {
for (let i = 0; i < RATE_LIMITS.mutation.max; i++) {
mutationMiddleware(req as Request, mockRes() as unknown as Response, () => {});
}
// 31st request should be blocked
// Next request should be blocked
const res = mockRes();
mutationMiddleware(req as Request, res as unknown as Response, () => {});
expect(res._status).toBe(429);
@@ -221,14 +221,13 @@ describe("rateLimit with RATE_LIMITS presets", () => {
it("works with RATE_LIMITS.sse preset", () => {
const sseMiddleware = rateLimit(RATE_LIMITS.sse);
const req = mockReq();
const res = mockRes();
// Exhaust limit
for (let i = 0; i < 10; i++) {
for (let i = 0; i < RATE_LIMITS.sse.max; i++) {
sseMiddleware(req as Request, mockRes() as unknown as Response, () => {});
}
// 11th request should be blocked
// Next request should be blocked
const blockedRes = mockRes();
sseMiddleware(req as Request, blockedRes as unknown as Response, () => {});
expect(blockedRes._status).toBe(429);

View File

@@ -77,10 +77,10 @@ export function rateLimit(options: RateLimitOptions = {}) {
/** Default rate limit configs for different endpoint patterns */
export const RATE_LIMITS = {
/** General API: 100 req/min */
api: { windowMs: 60_000, max: 100 },
/** Mutation endpoints (POST/PUT/PATCH/DELETE): 30 req/min */
mutation: { windowMs: 60_000, max: 30 },
/** SSE connections: 10 per minute */
sse: { windowMs: 60_000, max: 10 },
/** General API: 1000 req/min */
api: { windowMs: 60_000, max: 1000 },
/** Mutation endpoints (POST/PUT/PATCH/DELETE): 600 req/min */
mutation: { windowMs: 60_000, max: 600 },
/** SSE connections: 60 per minute */
sse: { windowMs: 60_000, max: 60 },
} as const;

View File

@@ -5,6 +5,7 @@ import http from "node:http";
import { createHmac } from "node:crypto";
import express from "express";
import { createServer, setupTerminalWebSocket } from "./server.js";
import { RATE_LIMITS } from "./rate-limit.js";
import { toSessionTag } from "./terminal-websocket-diagnostics.js";
import type { TaskStore } from "@fusion/core";
import { get as performGet, request as performRequest } from "./test-request.js";
@@ -438,7 +439,7 @@ describe("API Error Handling Middleware", () => {
it("still enforces the mutation rate-limit budget independently", async () => {
const app = createServer(store);
for (let i = 0; i < 30; i++) {
for (let i = 0; i < RATE_LIMITS.mutation.max; i++) {
const res = await REQUEST(
app,
"POST",
@@ -464,7 +465,7 @@ describe("API Error Handling Middleware", () => {
it("allows project setup mutations after the general mutation budget is exhausted", async () => {
const app = createServer(store);
for (let i = 0; i < 30; i++) {
for (let i = 0; i < RATE_LIMITS.mutation.max; i++) {
const res = await REQUEST(
app,
"POST",