diff --git a/utils/getClientIp.empty-fallback.test.ts b/utils/getClientIp.empty-fallback.test.ts new file mode 100644 index 000000000..814d1ef18 --- /dev/null +++ b/utils/getClientIp.empty-fallback.test.ts @@ -0,0 +1,47 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, expect, it } from 'vitest'; +import { getClientIp } from './getClientIp'; + +describe('getClientIp - Edge Cases & Empty/Missing Inputs Verification', () => { + it('1. returns fallback IP when request object is null or undefined', () => { + expect(getClientIp(null as any)).toBe('127.0.0.1'); + expect(getClientIp(undefined as any)).toBe('127.0.0.1'); + }); + + it('2. returns fallback IP when request has no headers property or headers is malformed', () => { + expect(getClientIp({} as any)).toBe('127.0.0.1'); + expect(getClientIp({ headers: null } as any)).toBe('127.0.0.1'); + expect(getClientIp({ headers: {} } as any)).toBe('127.0.0.1'); + expect(getClientIp({ headers: { get: 'not-a-function' } } as any)).toBe('127.0.0.1'); + }); + + it('3. returns fallback IP when options object is null or undefined', () => { + const req = new Request('http://localhost:3000/api/streak'); + expect(getClientIp(req, null as any)).toBe('127.0.0.1'); + expect(getClientIp(req, undefined as any)).toBe('127.0.0.1'); + }); + + it('4. behaves normally when options is empty object', () => { + const req = new Request('http://localhost:3000/api/streak'); + expect(getClientIp(req, {})).toBe('127.0.0.1'); + }); + + it('5. changes fallback behavior based on NODE_ENV (development/test vs production)', () => { + const originalEnv = process.env.NODE_ENV; + try { + (process.env as any).NODE_ENV = 'production'; + expect(getClientIp(null as any)).toBe('unknown'); + expect(getClientIp({} as any)).toBe('unknown'); + } finally { + (process.env as any).NODE_ENV = originalEnv; + } + }); + + it('6. handles malformed request and headers without throwing even if headersPriority is custom', () => { + expect( + getClientIp(null as any, { + headersPriority: ['x-custom-ip'], + }) + ).toBe('127.0.0.1'); + }); +}); diff --git a/utils/getClientIp.ts b/utils/getClientIp.ts index f127e28fc..a8f146bdf 100644 --- a/utils/getClientIp.ts +++ b/utils/getClientIp.ts @@ -57,8 +57,20 @@ export function getClientIp( request: Request | NextRequest, options: GetClientIpOptions = {} ): string { - const config = options.proxyConfig || loadTrustedProxyConfig(); + const opt = options || {}; + const isDevOrTest = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'; + const defaultIp = isDevOrTest ? '127.0.0.1' : 'unknown'; + + if (!request) { + return defaultIp; + } + const headers = request.headers; + if (!headers || typeof headers.get !== 'function') { + return defaultIp; + } + + const config = opt.proxyConfig || loadTrustedProxyConfig(); // 1. NextRequest has a secure, platform-populated request.ip property on Vercel/Next.js const requestIp = (request as unknown as { ip?: string }).ip; @@ -77,10 +89,10 @@ export function getClientIp( return requestIp; } - const directIp = options.directIp?.trim(); + const directIp = opt.directIp?.trim(); const forwardedHeaders = [ 'x-forwarded-for', - ...(options.headersPriority || ['x-vercel-proxied-for', 'cf-connecting-ip', 'x-real-ip']), + ...(opt.headersPriority || ['x-vercel-proxied-for', 'cf-connecting-ip', 'x-real-ip']), ]; // Forwarded headers cannot establish their own trust boundary. Without a @@ -159,7 +171,7 @@ export function getClientIp( } // 3. Custom/platform headers are accepted only behind a trusted direct peer. - const priorityHeaders = options.headersPriority || [ + const priorityHeaders = opt.headersPriority || [ 'x-vercel-proxied-for', 'cf-connecting-ip', 'x-real-ip',