From 2420e3f3164f412b1836dac9c5e474561741c296 Mon Sep 17 00:00:00 2001 From: Kokila-chandrakar Date: Mon, 8 Jun 2026 00:10:13 +0530 Subject: [PATCH 1/5] fix: rename middleware.ts to proxy.ts (Next.js 16.2.7 deprecation) --- middleware.ts => proxy.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename middleware.ts => proxy.ts (100%) diff --git a/middleware.ts b/proxy.ts similarity index 100% rename from middleware.ts rename to proxy.ts From ccaed7dd25ea53278c02d4e6f960ca672d6c7e79 Mon Sep 17 00:00:00 2001 From: Kokila-chandrakar Date: Mon, 8 Jun 2026 00:45:24 +0530 Subject: [PATCH 2/5] fix: delete middleware.ts from main to resolve conflict --- middleware.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 middleware.ts diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 000000000..ce262258e --- /dev/null +++ b/middleware.ts @@ -0,0 +1,64 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; +import { rateLimit } from './lib/rate-limit'; + +/** + * Middleware to enforce rate limiting on specific API routes. + * + * Protected Routes: + * - /api/streak + * - /api/github + * - /api/track-user + * - /api/stats + * - /api/og + * + * Limit: 60 requests per minute per IP. + */ +export async function middleware(request: NextRequest) { + // Use Vercel's ip property if available, fallback to headers, then localhost + const ip = + request.headers.get('x-forwarded-for')?.split(',')[0] ?? + request.headers.get('x-real-ip') ?? + '127.0.0.1'; + + // Apply rate limiting + // 60 requests per 60,000ms (1 minute) + const result = await rateLimit(ip, 60, 60000); + + if (!result.success) { + return NextResponse.json( + { error: 'Too many requests' }, + { + status: 429, + headers: { + 'Content-Type': 'application/json', + 'X-RateLimit-Limit': result.limit.toString(), + 'X-RateLimit-Remaining': result.remaining.toString(), + 'X-RateLimit-Reset': result.reset.toString(), + }, + } + ); + } + + // Add rate limit headers to the response for successful requests + const response = NextResponse.next(); + response.headers.set('X-RateLimit-Limit', result.limit.toString()); + response.headers.set('X-RateLimit-Remaining', result.remaining.toString()); + response.headers.set('X-RateLimit-Reset', result.reset.toString()); + + return response; +} + +/** + * Configure which routes should trigger this middleware. + * Using a matcher is more efficient than checking pathnames inside the middleware. + */ +export const config = { + matcher: [ + '/api/streak/:path*', + '/api/github/:path*', + '/api/track-user/:path*', + '/api/stats/:path*', + '/api/og/:path*', + ], +}; From 8e175ca854bfca6fb3553ef699ab2acb60a91318 Mon Sep 17 00:00:00 2001 From: Kokila-chandrakar Date: Thu, 11 Jun 2026 18:11:57 +0530 Subject: [PATCH 3/5] fix: rename exported middleware function to proxy --- middleware.ts | 74 --------------------------------------------------- proxy.ts | 2 +- 2 files changed, 1 insertion(+), 75 deletions(-) delete mode 100644 middleware.ts diff --git a/middleware.ts b/middleware.ts deleted file mode 100644 index a14616778..000000000 --- a/middleware.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; -import { rateLimit } from './lib/rate-limit'; -import { getClientIp } from './utils/getClientIp'; - -/** - * Next.js middleware — rate-limits all matched API routes. - * - * Next.js requires this file to be named `middleware.ts` at the project root - * and to export a function named `middleware` (and optionally `config`). - * - * @see https://nextjs.org/docs/app/building-your-application/routing/middleware - */ -export async function middleware(request: NextRequest): Promise { - const ip = getClientIp(request); - - const isRefresh = - request.nextUrl.searchParams.get('refresh') === 'true' || - request.nextUrl.searchParams.get('bypassCache') === 'true'; - - if (isRefresh) { - const refreshResult = await rateLimit(`refresh:${ip}`, 5, 60000); - - if (!refreshResult.success) { - const resp = NextResponse.json( - { error: 'Too many refresh requests. Please wait before bypassing the cache again.' }, - { status: 429 } - ); - resp.headers.set('X-RateLimit-Limit', refreshResult.limit.toString()); - resp.headers.set('X-RateLimit-Remaining', '0'); - resp.headers.set('X-RateLimit-Reset', refreshResult.reset.toString()); - resp.headers.set('X-RateLimit-Policy', 'refresh'); - return resp; - } - } - - const result = await rateLimit(ip, 60, 60000); - - if (!result.success) { - return NextResponse.json( - { error: 'Too many requests' }, - { - status: 429, - headers: { - 'Content-Type': 'application/json', - 'X-RateLimit-Limit': result.limit.toString(), - 'X-RateLimit-Remaining': result.remaining.toString(), - 'X-RateLimit-Reset': result.reset.toString(), - }, - } - ); - } - - const response = NextResponse.next(); - response.headers.set('X-RateLimit-Limit', result.limit.toString()); - response.headers.set('X-RateLimit-Remaining', result.remaining.toString()); - response.headers.set('X-RateLimit-Reset', result.reset.toString()); - - return response; -} - -export const config = { - matcher: [ - '/api/streak/:path*', - '/api/github/:path*', - '/api/track-user/:path*', - '/api/stats/:path*', - '/api/og/:path*', - '/api/notify/:path*', - '/api/compare/:path*', - '/api/wrapped/:path*', - '/api/student/:path*', - ], -}; diff --git a/proxy.ts b/proxy.ts index ce262258e..8d2f1e43a 100644 --- a/proxy.ts +++ b/proxy.ts @@ -14,7 +14,7 @@ import { rateLimit } from './lib/rate-limit'; * * Limit: 60 requests per minute per IP. */ -export async function middleware(request: NextRequest) { +export async function proxy(request: NextRequest) { // Use Vercel's ip property if available, fallback to headers, then localhost const ip = request.headers.get('x-forwarded-for')?.split(',')[0] ?? From 43b1fcaed035fc5db432a4f11e428dd4744095c8 Mon Sep 17 00:00:00 2001 From: Kokila-chandrakar Date: Thu, 11 Jun 2026 18:14:01 +0530 Subject: [PATCH 4/5] chore: resolve merge conflict, remove middleware.ts in favor of proxy.ts --- package-lock.json | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index b3bc3ed3f..64713caff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1885,9 +1885,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1904,9 +1901,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1923,9 +1917,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1942,9 +1933,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1961,9 +1949,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3282,7 +3267,7 @@ "version": "19.2.16", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.16.tgz", "integrity": "sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -9641,11 +9626,6 @@ } }, "node_modules/react-is": { - - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", - "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", - "dev": true, "version": "19.2.6", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", From a0da39318f7890a636c1f4b471961e6686ad0b33 Mon Sep 17 00:00:00 2001 From: Kokila-chandrakar Date: Thu, 11 Jun 2026 18:23:11 +0530 Subject: [PATCH 5/5] fix: rename middleware test files to proxy and update imports --- ...ity.test.ts => proxy.accessibility.test.ts | 14 ++-- middleware.test.ts => proxy.test.ts | 66 +++++++++---------- 2 files changed, 40 insertions(+), 40 deletions(-) rename middleware.accessibility.test.ts => proxy.accessibility.test.ts (87%) rename middleware.test.ts => proxy.test.ts (88%) diff --git a/middleware.accessibility.test.ts b/proxy.accessibility.test.ts similarity index 87% rename from middleware.accessibility.test.ts rename to proxy.accessibility.test.ts index c825057df..76744674f 100644 --- a/middleware.accessibility.test.ts +++ b/proxy.accessibility.test.ts @@ -1,13 +1,13 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { NextRequest, NextResponse } from 'next/server'; -import { middleware } from './middleware'; +import { proxy } from './proxy'; import { rateLimit } from './lib/rate-limit'; vi.mock('./lib/rate-limit', () => ({ rateLimit: vi.fn(), })); -describe('proxy.accessibility - Middleware Responsibilities (JSON responses, rate limits, headers)', () => { +describe('proxy.accessibility - proxy Responsibilities (JSON responses, rate limits, headers)', () => { beforeEach(() => { vi.clearAllMocks(); }); @@ -21,7 +21,7 @@ describe('proxy.accessibility - Middleware Responsibilities (JSON responses, rat }); const request = new NextRequest('http://localhost:3000/api/streak'); - const response = await middleware(request); + const response = await proxy(request); expect(response.status).toBe(429); const body = await response.json(); @@ -37,7 +37,7 @@ describe('proxy.accessibility - Middleware Responsibilities (JSON responses, rat }); const request = new NextRequest('http://localhost:3000/api/streak?refresh=true'); - const response = await middleware(request); + const response = await proxy(request); expect(response.status).toBe(429); const body = await response.json(); @@ -55,7 +55,7 @@ describe('proxy.accessibility - Middleware Responsibilities (JSON responses, rat }); const request = new NextRequest('http://localhost:3000/api/streak'); - const response = await middleware(request); + const response = await proxy(request); expect(response.headers.get('X-RateLimit-Limit')).toBe('60'); expect(response.headers.get('X-RateLimit-Remaining')).toBe('59'); @@ -71,7 +71,7 @@ describe('proxy.accessibility - Middleware Responsibilities (JSON responses, rat }); const request = new NextRequest('http://localhost:3000/api/streak?bypassCache=true'); - const response = await middleware(request); + const response = await proxy(request); expect(response.headers.get('X-RateLimit-Limit')).toBe('5'); expect(response.headers.get('X-RateLimit-Remaining')).toBe('0'); @@ -89,7 +89,7 @@ describe('proxy.accessibility - Middleware Responsibilities (JSON responses, rat const nextSpy = vi.spyOn(NextResponse, 'next'); const request = new NextRequest('http://localhost:3000/api/streak'); - await middleware(request); + await proxy(request); expect(nextSpy).toHaveBeenCalled(); }); diff --git a/middleware.test.ts b/proxy.test.ts similarity index 88% rename from middleware.test.ts rename to proxy.test.ts index 07a8a65a4..87081e229 100644 --- a/middleware.test.ts +++ b/proxy.test.ts @@ -1,6 +1,6 @@ import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; import { NextRequest, NextResponse } from 'next/server'; -import { middleware, config } from './middleware'; +import { proxy, config } from './proxy'; import { rateLimit } from '@/lib/rate-limit'; vi.mock('@/lib/rate-limit', () => ({ @@ -19,7 +19,7 @@ function mockBothLimiters( }); } -describe('middleware', () => { +describe('proxy', () => { let originalEnv: string | undefined; beforeEach(() => { @@ -43,7 +43,7 @@ describe('middleware', () => { const nextSpy = vi.spyOn(NextResponse, 'next'); const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); - await middleware(request); + await proxy(request); expect(nextSpy).toHaveBeenCalled(); }); @@ -57,7 +57,7 @@ describe('middleware', () => { }); const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); - const response = await middleware(request); + const response = await proxy(request); expect(response.status).toBe(429); }); @@ -71,7 +71,7 @@ describe('middleware', () => { }); const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); - const response = await middleware(request); + const response = await proxy(request); await expect(response.json()).resolves.toEqual({ error: 'Too many requests', @@ -87,7 +87,7 @@ describe('middleware', () => { }); const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); - await middleware(request); + await proxy(request); expect(rateLimit).toHaveBeenCalledWith(expect.any(String), 60, 60000); }); @@ -101,7 +101,7 @@ describe('middleware', () => { }); const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); - const response = await middleware(request); + const response = await proxy(request); expect(response.headers.get('X-RateLimit-Limit')).toBe('60'); expect(response.headers.get('X-RateLimit-Remaining')).toBe('59'); @@ -117,7 +117,7 @@ describe('middleware', () => { }); const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); - const response = await middleware(request); + const response = await proxy(request); expect(response.headers.get('X-RateLimit-Limit')).toBe('60'); expect(response.headers.get('X-RateLimit-Remaining')).toBe('58'); @@ -133,7 +133,7 @@ describe('middleware', () => { }); const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); - const response = await middleware(request); + const response = await proxy(request); expect(response.headers.get('Content-Type')).toBe('application/json'); expect(response.headers.get('X-RateLimit-Limit')).toBe('60'); @@ -155,7 +155,7 @@ describe('middleware', () => { }, }); - await middleware(request); + await proxy(request); expect(rateLimit).toHaveBeenCalledWith('1.2.3.4', 60, 60000); }); @@ -176,7 +176,7 @@ describe('middleware', () => { }, }); - await middleware(request); + await proxy(request); expect(rateLimit).toHaveBeenCalledWith('5.6.7.8', 60, 60000); }); @@ -195,7 +195,7 @@ describe('middleware', () => { }, }); - await middleware(request); + await proxy(request); expect(rateLimit).toHaveBeenCalledWith('9.9.9.9', 60, 60000); }); @@ -210,7 +210,7 @@ describe('middleware', () => { const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); - await middleware(request); + await proxy(request); expect(rateLimit).toHaveBeenCalledWith('127.0.0.1', 60, 60000); }); @@ -230,7 +230,7 @@ describe('middleware', () => { }, }); - await middleware(request); + await proxy(request); // Expect 9.9.9.9 instead of 1.2.3.4 because x-real-ip is more secure expect(rateLimit).toHaveBeenCalledWith('9.9.9.9', 60, 60000); @@ -250,7 +250,7 @@ describe('middleware', () => { }, }); - await middleware(request); + await proxy(request); expect(rateLimit).toHaveBeenCalledWith('1.2.3.4', 60, 60000); }); @@ -269,7 +269,7 @@ describe('middleware', () => { mockBothLimiters({ success: true, limit: 5, remaining: 4, reset: 123456789 }); const request = new NextRequest('http://localhost:3000/api/streak?user=octocat&refresh=true'); - await middleware(request); + await proxy(request); expect(rateLimit).toHaveBeenNthCalledWith(1, 'refresh:127.0.0.1', 5, 60000); expect(rateLimit).toHaveBeenNthCalledWith(2, '127.0.0.1', 60, 60000); @@ -279,7 +279,7 @@ describe('middleware', () => { mockBothLimiters({ success: false, limit: 5, remaining: 0, reset: 123456789 }); const request = new NextRequest('http://localhost:3000/api/streak?user=octocat&refresh=true'); - const response = await middleware(request); + const response = await proxy(request); expect(response.status).toBe(429); const body = await response.json(); @@ -295,7 +295,7 @@ describe('middleware', () => { }); const request = new NextRequest('http://localhost:3000/api/streak?user=octocat&refresh=true'); - const response = await middleware(request); + const response = await proxy(request); expect(response.headers.get('X-RateLimit-Limit')).toBe('5'); expect(response.status).toBe(429); @@ -310,7 +310,7 @@ describe('middleware', () => { }); const request = new NextRequest('http://localhost:3000/api/streak?user=octocat&refresh=true'); - await middleware(request); + await proxy(request); expect(rateLimit).not.toHaveBeenCalledWith('127.0.0.1', 60, 60000); }); @@ -324,7 +324,7 @@ describe('middleware', () => { }); const request = new NextRequest('http://localhost:3000/api/streak?user=octocat&refresh=true'); - const response = await middleware(request); + const response = await proxy(request); expect(response.headers.get('X-RateLimit-Limit')).toBe('5'); expect(response.headers.get('X-RateLimit-Remaining')).toBe('0'); @@ -339,7 +339,7 @@ describe('middleware', () => { }); const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); - await middleware(request); + await proxy(request); expect(rateLimit).toHaveBeenCalledTimes(1); expect(rateLimit).toHaveBeenCalledWith('127.0.0.1', 60, 60000); @@ -356,7 +356,7 @@ describe('middleware', () => { const request = new NextRequest( 'http://localhost:3000/api/streak?user=octocat&refresh=false' ); - await middleware(request); + await proxy(request); expect(rateLimit).toHaveBeenCalledTimes(1); expect(rateLimit).not.toHaveBeenCalledWith(expect.stringContaining('refresh:'), 5, 60000); @@ -366,7 +366,7 @@ describe('middleware', () => { mockBothLimiters({ success: true, limit: 5, remaining: 3, reset: 123456789 }); const request = new NextRequest('http://localhost:3000/api/streak?user=octocat&refresh=true'); - const response = await middleware(request); + const response = await proxy(request); expect(rateLimit).toHaveBeenCalledTimes(2); expect(response.status).toBe(200); @@ -378,7 +378,7 @@ describe('middleware', () => { .mockResolvedValueOnce({ success: false, limit: 60, remaining: 0, reset: 123456789 }); const request = new NextRequest('http://localhost:3000/api/streak?user=octocat&refresh=true'); - const response = await middleware(request); + const response = await proxy(request); expect(response.status).toBe(429); const body = await response.json(); @@ -387,24 +387,24 @@ describe('middleware', () => { }); }); -describe('middleware.ts wiring', () => { - it('middleware.ts exports a function named middleware', async () => { - const mod = await import('./middleware'); +describe('proxy.ts wiring', () => { + it('proxy.ts exports a function named proxy', async () => { + const mod = await import('./proxy'); - // Next.js looks for a named export called `middleware` - expect(typeof mod.middleware).toBe('function'); + // Next.js looks for a named export called `proxy` + expect(typeof mod.proxy).toBe('function'); }); - it('middleware.ts exports config with a non-empty matcher array', async () => { - const mod = await import('./middleware'); + it('proxy.ts exports config with a non-empty matcher array', async () => { + const mod = await import('./proxy'); expect(mod.config).toBeDefined(); expect(Array.isArray(mod.config.matcher)).toBe(true); expect(mod.config.matcher.length).toBeGreaterThan(0); }); - it('middleware covers all expected API routes', async () => { - const { config: mwConfig } = await import('./middleware'); + it('proxy covers all expected API routes', async () => { + const { config: mwConfig } = await import('./proxy'); const expected = [ '/api/streak/:path*', '/api/github/:path*',