From 83606400e7efcf4c214c6dd98234596bcc07e5b1 Mon Sep 17 00:00:00 2001 From: Kokila-chandrakar Date: Fri, 12 Jun 2026 03:00:31 +0530 Subject: [PATCH 1/4] fix: rename middleware.ts to proxy.ts (Next.js 16.2.7 deprecation) --- middleware.test.ts => proxy.test.ts | 2 +- middleware.ts => proxy.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename middleware.test.ts => proxy.test.ts (99%) rename middleware.ts => proxy.ts (96%) diff --git a/middleware.test.ts b/proxy.test.ts similarity index 99% rename from middleware.test.ts rename to proxy.test.ts index 2a95fe74a..e9879bab7 100644 --- a/middleware.test.ts +++ b/proxy.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } 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', () => ({ diff --git a/middleware.ts b/proxy.ts similarity index 96% rename from middleware.ts rename to proxy.ts index ce262258e..8d2f1e43a 100644 --- a/middleware.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 83b93b9ff726ee1791259779ffa5b97cfd3b3c44 Mon Sep 17 00:00:00 2001 From: Kokila-chandrakar Date: Fri, 12 Jun 2026 03:07:26 +0530 Subject: [PATCH 2/4] fix: update function calls and rename middleware to proxy --- proxy.test.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/proxy.test.ts b/proxy.test.ts index e9879bab7..a5dd8be2e 100644 --- a/proxy.test.ts +++ b/proxy.test.ts @@ -7,7 +7,7 @@ vi.mock('@/lib/rate-limit', () => ({ rateLimit: vi.fn(), })); -describe('middleware', () => { +describe('proxy', () => { beforeEach(() => { vi.clearAllMocks(); }); @@ -23,7 +23,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(); }); @@ -37,7 +37,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); }); @@ -51,7 +51,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', @@ -67,7 +67,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'); }); @@ -81,7 +81,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-Remaining')).toBe('59'); }); @@ -100,7 +100,7 @@ describe('middleware', () => { }, }); - await middleware(request); + await proxy(request); expect(rateLimit).toHaveBeenCalledWith('1.2.3.4', 60, 60000); }); @@ -119,7 +119,7 @@ describe('middleware', () => { }, }); - await middleware(request); + await proxy(request); expect(rateLimit).toHaveBeenCalledWith('9.9.9.9', 60, 60000); }); @@ -134,7 +134,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); }); @@ -154,7 +154,7 @@ describe('middleware', () => { }, }); - await middleware(request); + await proxy(request); expect(rateLimit).toHaveBeenCalledWith('1.2.3.4', 60, 60000); }); @@ -173,7 +173,7 @@ describe('middleware', () => { }, }); - await middleware(request); + await proxy(request); expect(rateLimit).toHaveBeenCalledWith('1.2.3.4', 60, 60000); }); From 32d9fa24209526e55cbebc80bdf1257334c51725 Mon Sep 17 00:00:00 2001 From: Kokila-chandrakar Date: Fri, 12 Jun 2026 03:09:59 +0530 Subject: [PATCH 3/4] fix: explicitly remove middleware files to resolve conflict --- middleware.test.ts | 180 +++++++++++++++++++++++++++++++++++++++++++++ middleware.ts | 64 ++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 middleware.test.ts create mode 100644 middleware.ts diff --git a/middleware.test.ts b/middleware.test.ts new file mode 100644 index 000000000..2a95fe74a --- /dev/null +++ b/middleware.test.ts @@ -0,0 +1,180 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { NextRequest, NextResponse } from 'next/server'; +import { middleware } from './middleware'; +import { rateLimit } from '@/lib/rate-limit'; + +vi.mock('@/lib/rate-limit', () => ({ + rateLimit: vi.fn(), +})); + +describe('middleware', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('calls NextResponse.next when rate limit succeeds', async () => { + vi.mocked(rateLimit).mockResolvedValue({ + success: true, + limit: 60, + remaining: 59, + reset: 123456789, + }); + + const nextSpy = vi.spyOn(NextResponse, 'next'); + + const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); + await middleware(request); + + expect(nextSpy).toHaveBeenCalled(); + }); + + it('returns 429 when rate limit fails', async () => { + vi.mocked(rateLimit).mockResolvedValue({ + success: false, + limit: 60, + remaining: 0, + reset: 123456789, + }); + + const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); + const response = await middleware(request); + + expect(response.status).toBe(429); + }); + + it('returns too many requests error body when rate limit fails', async () => { + vi.mocked(rateLimit).mockResolvedValue({ + success: false, + limit: 60, + remaining: 0, + reset: 123456789, + }); + + const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); + const response = await middleware(request); + + await expect(response.json()).resolves.toEqual({ + error: 'Too many requests', + }); + }); + + it('sets X-RateLimit-Limit header when rate limit succeeds', async () => { + vi.mocked(rateLimit).mockResolvedValue({ + success: true, + limit: 60, + remaining: 59, + reset: 123456789, + }); + + const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); + const response = await middleware(request); + + expect(response.headers.get('X-RateLimit-Limit')).toBe('60'); + }); + + it('sets X-RateLimit-Remaining header when rate limit succeeds', async () => { + vi.mocked(rateLimit).mockResolvedValue({ + success: true, + limit: 60, + remaining: 59, + reset: 123456789, + }); + + const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); + const response = await middleware(request); + + expect(response.headers.get('X-RateLimit-Remaining')).toBe('59'); + }); + + it('uses first IP from x-forwarded-for', async () => { + vi.mocked(rateLimit).mockResolvedValue({ + success: true, + limit: 60, + remaining: 59, + reset: 123456789, + }); + + const request = new NextRequest('http://localhost:3000/api/streak?user=octocat', { + headers: { + 'x-forwarded-for': '1.2.3.4, 5.6.7.8', + }, + }); + + await middleware(request); + + expect(rateLimit).toHaveBeenCalledWith('1.2.3.4', 60, 60000); + }); + + it('uses x-real-ip if x-forwarded-for is missing', async () => { + vi.mocked(rateLimit).mockResolvedValue({ + success: true, + limit: 60, + remaining: 59, + reset: 123456789, + }); + + const request = new NextRequest('http://localhost:3000/api/streak?user=octocat', { + headers: { + 'x-real-ip': '9.9.9.9', + }, + }); + + await middleware(request); + + expect(rateLimit).toHaveBeenCalledWith('9.9.9.9', 60, 60000); + }); + + it('defaults to 127.0.0.1 when no IP headers', async () => { + vi.mocked(rateLimit).mockResolvedValue({ + success: true, + limit: 60, + remaining: 59, + reset: 123456789, + }); + + const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); + + await middleware(request); + + expect(rateLimit).toHaveBeenCalledWith('127.0.0.1', 60, 60000); + }); + + it('prefers x-forwarded-for over x-real-ip', async () => { + vi.mocked(rateLimit).mockResolvedValue({ + success: true, + limit: 60, + remaining: 59, + reset: 123456789, + }); + + const request = new NextRequest('http://localhost:3000/api/streak?user=octocat', { + headers: { + 'x-forwarded-for': '1.2.3.4, 5.6.7.8', + 'x-real-ip': '9.9.9.9', + }, + }); + + await middleware(request); + + expect(rateLimit).toHaveBeenCalledWith('1.2.3.4', 60, 60000); + }); + + it('handles multiple IPs with whitespace', async () => { + vi.mocked(rateLimit).mockResolvedValue({ + success: true, + limit: 60, + remaining: 59, + reset: 123456789, + }); + + const request = new NextRequest('http://localhost:3000/api/streak?user=octocat', { + headers: { + 'x-forwarded-for': '1.2.3.4, 5.6.7.8, 9.10.11.12', + }, + }); + + await middleware(request); + + expect(rateLimit).toHaveBeenCalledWith('1.2.3.4', 60, 60000); + }); +}); 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 91a0188159ce627f9818fd0a0e96723875046fa8 Mon Sep 17 00:00:00 2001 From: Kokila-chandrakar Date: Fri, 12 Jun 2026 03:14:07 +0530 Subject: [PATCH 4/4] fix: remove middleware.ts and middleware.test.ts, keep proxy files only --- middleware.test.ts | 180 --------------------------------------------- middleware.ts | 64 ---------------- 2 files changed, 244 deletions(-) delete mode 100644 middleware.test.ts delete mode 100644 middleware.ts diff --git a/middleware.test.ts b/middleware.test.ts deleted file mode 100644 index 2a95fe74a..000000000 --- a/middleware.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { NextRequest, NextResponse } from 'next/server'; -import { middleware } from './middleware'; -import { rateLimit } from '@/lib/rate-limit'; - -vi.mock('@/lib/rate-limit', () => ({ - rateLimit: vi.fn(), -})); - -describe('middleware', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('calls NextResponse.next when rate limit succeeds', async () => { - vi.mocked(rateLimit).mockResolvedValue({ - success: true, - limit: 60, - remaining: 59, - reset: 123456789, - }); - - const nextSpy = vi.spyOn(NextResponse, 'next'); - - const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); - await middleware(request); - - expect(nextSpy).toHaveBeenCalled(); - }); - - it('returns 429 when rate limit fails', async () => { - vi.mocked(rateLimit).mockResolvedValue({ - success: false, - limit: 60, - remaining: 0, - reset: 123456789, - }); - - const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); - const response = await middleware(request); - - expect(response.status).toBe(429); - }); - - it('returns too many requests error body when rate limit fails', async () => { - vi.mocked(rateLimit).mockResolvedValue({ - success: false, - limit: 60, - remaining: 0, - reset: 123456789, - }); - - const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); - const response = await middleware(request); - - await expect(response.json()).resolves.toEqual({ - error: 'Too many requests', - }); - }); - - it('sets X-RateLimit-Limit header when rate limit succeeds', async () => { - vi.mocked(rateLimit).mockResolvedValue({ - success: true, - limit: 60, - remaining: 59, - reset: 123456789, - }); - - const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); - const response = await middleware(request); - - expect(response.headers.get('X-RateLimit-Limit')).toBe('60'); - }); - - it('sets X-RateLimit-Remaining header when rate limit succeeds', async () => { - vi.mocked(rateLimit).mockResolvedValue({ - success: true, - limit: 60, - remaining: 59, - reset: 123456789, - }); - - const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); - const response = await middleware(request); - - expect(response.headers.get('X-RateLimit-Remaining')).toBe('59'); - }); - - it('uses first IP from x-forwarded-for', async () => { - vi.mocked(rateLimit).mockResolvedValue({ - success: true, - limit: 60, - remaining: 59, - reset: 123456789, - }); - - const request = new NextRequest('http://localhost:3000/api/streak?user=octocat', { - headers: { - 'x-forwarded-for': '1.2.3.4, 5.6.7.8', - }, - }); - - await middleware(request); - - expect(rateLimit).toHaveBeenCalledWith('1.2.3.4', 60, 60000); - }); - - it('uses x-real-ip if x-forwarded-for is missing', async () => { - vi.mocked(rateLimit).mockResolvedValue({ - success: true, - limit: 60, - remaining: 59, - reset: 123456789, - }); - - const request = new NextRequest('http://localhost:3000/api/streak?user=octocat', { - headers: { - 'x-real-ip': '9.9.9.9', - }, - }); - - await middleware(request); - - expect(rateLimit).toHaveBeenCalledWith('9.9.9.9', 60, 60000); - }); - - it('defaults to 127.0.0.1 when no IP headers', async () => { - vi.mocked(rateLimit).mockResolvedValue({ - success: true, - limit: 60, - remaining: 59, - reset: 123456789, - }); - - const request = new NextRequest('http://localhost:3000/api/streak?user=octocat'); - - await middleware(request); - - expect(rateLimit).toHaveBeenCalledWith('127.0.0.1', 60, 60000); - }); - - it('prefers x-forwarded-for over x-real-ip', async () => { - vi.mocked(rateLimit).mockResolvedValue({ - success: true, - limit: 60, - remaining: 59, - reset: 123456789, - }); - - const request = new NextRequest('http://localhost:3000/api/streak?user=octocat', { - headers: { - 'x-forwarded-for': '1.2.3.4, 5.6.7.8', - 'x-real-ip': '9.9.9.9', - }, - }); - - await middleware(request); - - expect(rateLimit).toHaveBeenCalledWith('1.2.3.4', 60, 60000); - }); - - it('handles multiple IPs with whitespace', async () => { - vi.mocked(rateLimit).mockResolvedValue({ - success: true, - limit: 60, - remaining: 59, - reset: 123456789, - }); - - const request = new NextRequest('http://localhost:3000/api/streak?user=octocat', { - headers: { - 'x-forwarded-for': '1.2.3.4, 5.6.7.8, 9.10.11.12', - }, - }); - - await middleware(request); - - expect(rateLimit).toHaveBeenCalledWith('1.2.3.4', 60, 60000); - }); -}); diff --git a/middleware.ts b/middleware.ts deleted file mode 100644 index ce262258e..000000000 --- a/middleware.ts +++ /dev/null @@ -1,64 +0,0 @@ -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*', - ], -};