Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/__tests__/jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ global.console = {

jest.setTimeout(20000);

// Remove native fetch so tests can mock node-fetch
// This ensures backward compatibility with existing tests that mock node-fetch
delete global.fetch;

const mock_gateSpec = {
name: 'nfl_gate',
type: 'feature_gate',
Expand Down
116 changes: 116 additions & 0 deletions src/utils/__tests__/safeFetch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Tests for safeFetch module - verifies native fetch is preferred over node-fetch
* when available, eliminating the url.parse() deprecation warning in Node 18+.
*/

describe('safeFetch', () => {
const originalFetch = global.fetch;

afterEach(() => {
// Restore original state
if (originalFetch) {
global.fetch = originalFetch;
} else {
delete (global as any).fetch;
}
jest.resetModules();
});

describe('when native fetch is available', () => {
it('should use native fetch instead of node-fetch', async () => {
// Setup: Create a mock native fetch
const mockNativeFetch = jest.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ source: 'native' }),
});
(global as any).fetch = mockNativeFetch;

// Clear module cache to re-evaluate safeFetch with native fetch available
jest.resetModules();

// Import fresh copy of safeFetch
const { default: safeFetch } = await import('../safeFetch');

// Act
await safeFetch('https://example.com/test');

// Assert: native fetch should be called
expect(mockNativeFetch).toHaveBeenCalledTimes(1);
expect(mockNativeFetch).toHaveBeenCalledWith('https://example.com/test');
});

it('should not require node-fetch when native fetch exists', async () => {
// Setup: Create a mock native fetch
const mockNativeFetch = jest.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ source: 'native' }),
});
(global as any).fetch = mockNativeFetch;

// Spy on require to verify node-fetch is not loaded
const originalRequire = jest.requireActual;

jest.resetModules();

// Import fresh copy
const { default: safeFetch } = await import('../safeFetch');

// Act
await safeFetch('https://example.com/test');

// Assert: should work without errors using native fetch
expect(mockNativeFetch).toHaveBeenCalled();
});
});

describe('when native fetch is NOT available', () => {
it('should fall back to node-fetch', async () => {
// Setup: Remove native fetch
delete (global as any).fetch;

// Mock node-fetch
const mockNodeFetch = jest.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ source: 'node-fetch' }),
});

jest.resetModules();
jest.doMock('node-fetch', () => mockNodeFetch);

// Import fresh copy of safeFetch
const { default: safeFetch } = await import('../safeFetch');

// Act
await safeFetch('https://example.com/test');

// Assert: node-fetch should be called
expect(mockNodeFetch).toHaveBeenCalledTimes(1);
});
});

describe('fetch implementation selection', () => {
it('should prioritize native fetch over node-fetch for Node 18+ compatibility', async () => {
// This test documents the expected behavior:
// Native fetch (Node 18+) should be preferred because:
// 1. It doesn't use the deprecated url.parse() API
// 2. It's built into Node.js, reducing dependencies
// 3. It has better performance characteristics

const mockNativeFetch = jest.fn().mockResolvedValue({ ok: true });
const mockNodeFetch = jest.fn().mockResolvedValue({ ok: true });

(global as any).fetch = mockNativeFetch;

jest.resetModules();
jest.doMock('node-fetch', () => mockNodeFetch);

const { default: safeFetch } = await import('../safeFetch');

await safeFetch('https://example.com');

// Native fetch should be used, not node-fetch
expect(mockNativeFetch).toHaveBeenCalled();
expect(mockNodeFetch).not.toHaveBeenCalled();
});
});
});
32 changes: 22 additions & 10 deletions src/utils/safeFetch.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,38 @@
import { GlobalContext } from './StatsigContext';

// @ts-ignore
let nodeFetch: (...args) => Promise<Response> = null;
let fetchImpl: ((...args) => Promise<Response>) | null = null;

// Check if native fetch is available (Node 18+, browsers, edge runtimes)
// @ts-ignore
if (typeof fetch === 'function') {
// @ts-ignore
fetchImpl = fetch;
}

// Only fall back to node-fetch if native fetch is not available
// and we're not in an edge environment
// @ts-ignore
if (!GlobalContext.isEdgeEnvironment) {
if (!fetchImpl && !GlobalContext.isEdgeEnvironment) {
try {
nodeFetch = require('node-fetch');
const nodeFetch = require('node-fetch');
const nfDefault = (nodeFetch as any).default;
if (nfDefault && typeof nfDefault === 'function') {
nodeFetch = nfDefault;
fetchImpl = nfDefault;
} else {
fetchImpl = nodeFetch;
}
} catch (err) {
// Ignore
// Ignore - fetch might be provided by the runtime
}
}

// @ts-ignore
export default function safeFetch(...args): Promise<Response> {
if (nodeFetch) {
return nodeFetch(...args);
} else {
// @ts-ignore
return fetch(...args);
if (fetchImpl) {
return fetchImpl(...args);
}
// Last resort: try global fetch (might throw if not available)
// @ts-ignore
return fetch(...args);
}