From d7d7716890cf1c4cc968f5ffb70416eda89ee8d4 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 12 Oct 2026 21:11:46 -0400 Subject: [PATCH] fix(app): stop retaining timestamps for rejected runtime requests --- .../api/copilotkit/limit-regression.test.ts | 19 +++++++++++++++++++ apps/app/src/app/api/copilotkit/route.ts | 4 ++++ 2 files changed, 23 insertions(+) create mode 100644 apps/app/src/app/api/copilotkit/limit-regression.test.ts diff --git a/apps/app/src/app/api/copilotkit/limit-regression.test.ts b/apps/app/src/app/api/copilotkit/limit-regression.test.ts new file mode 100644 index 0000000..4ab5405 --- /dev/null +++ b/apps/app/src/app/api/copilotkit/limit-regression.test.ts @@ -0,0 +1,19 @@ +// @vitest-environment node +import { afterEach, it, expect, vi } from "vitest"; +import { NextRequest } from "next/server"; +vi.mock("@copilotkit/runtime",()=>({CopilotRuntime:class {}, ExperimentalEmptyAdapter:class {},copilotRuntimeNextJSAppRouterEndpoint:()=>({handleRequest:async()=>new Response(null)})})); +vi.mock("@/lib/copilotkit-runtime-options",()=>({buildRuntimeOptions:()=>({})})); +vi.mock("@/lib/launch-telemetry",()=>({getLaunchTelemetry:()=>({activated:true,runtimeProperties:()=>undefined,recordOpened:()=>Promise.resolve()}),runtimeMethod:()=>undefined,observeRunResult:()=>Promise.resolve(false)})); +vi.mock("next/server",async importOriginal=>({...await importOriginal(),after:vi.fn()})); +afterEach(()=>{vi.useRealTimers();vi.unstubAllEnvs();vi.restoreAllMocks();vi.resetModules();}); +it("retains only accepted request timestamps during a rejected burst", async () => { + vi.useFakeTimers(); vi.stubEnv("RATE_LIMIT_ENABLED","true"); vi.stubEnv("RATE_LIMIT_MAX","2"); + const arrays: unknown[][]=[]; const realSet=Map.prototype.set; + vi.spyOn(Map.prototype,"set").mockImplementation(function(this:Map,key:unknown,value:unknown){if(key==="198.51.100.1"&&Array.isArray(value)) arrays.push(value);return realSet.call(this,key,value);}); + const { POST }=await import("./route"); + const request=()=>new NextRequest("http://localhost/api/copilotkit",{method:"POST",headers:{"x-forwarded-for":"198.51.100.1"}}); + expect((await POST(request())).status).toBe(200); expect((await POST(request())).status).toBe(200); + for(let i=0;i<50;i++)expect((await POST(request())).status).toBe(429); + expect(Math.max(...arrays.map(a=>a.length))).toBe(2); + vi.advanceTimersByTime(60001); expect((await POST(request())).status).toBe(200); +}); diff --git a/apps/app/src/app/api/copilotkit/route.ts b/apps/app/src/app/api/copilotkit/route.ts index f16873b..1fbef46 100644 --- a/apps/app/src/app/api/copilotkit/route.ts +++ b/apps/app/src/app/api/copilotkit/route.ts @@ -20,6 +20,10 @@ function isRateLimited(ip: string): boolean { if (!RATE_LIMIT_ENABLED) return false; const now = Date.now(); const timestamps = hits.get(ip)?.filter(t => t > now - RATE_LIMIT_WINDOW_MS) ?? []; + if (timestamps.length >= RATE_LIMIT_MAX) { + hits.set(ip, timestamps); + return true; + } timestamps.push(now); hits.set(ip, timestamps); return timestamps.length > RATE_LIMIT_MAX;