Node Fetch: A Technical Guide to HTTP Requests and Proxy Integration

The HTTP request constitutes the fundamental unit of communication in modern web development. Browsers use them to render pages, servers exchange them to coordinate distributed systems, and applications rely on them to consume APIs and external services. For Node.js developers, the ability to make these requests programmatically is an essential skill that underpins everything from data collection to service integration.

The Fetch API, standardized by the WHATWG, has emerged as the modern approach to handling HTTP requests in JavaScript environments. What began as a browser-only specification has become a native feature of Node.js, eliminating the need for third-party libraries in many scenarios. Yet the Fetch API presents a specific challenge when network requests must traverse proxy servers—a requirement that arises in corporate environments, for bypassing geographic restrictions, or when building web scraping pipelines.

This guide examines the technical landscape of HTTP requests in Node.js using the Fetch API, with particular attention to proxy integration. It explores the evolution of Fetch in Node.js, compares the available implementation approaches, and provides comprehensive code examples for each method. The discussion encompasses environment-based configuration, custom agent implementation, Undici’s ProxyAgent, and the global-agent package, equipping developers with the knowledge to select and implement the appropriate solution for their specific use case.

Understanding the Fetch API in Node.js

The Evolution of HTTP Requests in Node.js

Node.js has supported HTTP requests since its earliest versions through the built-in http and https modules. These modules, while powerful and flexible, rely on a callback-based API that can lead to nested, difficult-to-read code when handling complex request sequences.

The Fetch API represented a paradigm shift, introducing a promise-based mechanism that handles asynchronous operations gracefully. When a request is made, Fetch returns a Promise that resolves with the server’s response, enabling developers to use async/await syntax for cleaner, more maintainable code.

The journey of Fetch to Node.js began with the node-fetch library, a lightweight implementation that brought the browser’s Fetch API syntax to server-side JavaScript. This library gained widespread adoption before Node.js added native support.

Native Fetch was shipped experimentally in Node.js 18 and became stable from Node.js 21. By Node.js 22 and 24, the implementation had matured significantly, with built-in proxy support arriving in version 22.21.0 and receiving further enhancements in 24.0.0 and beyond.

Native Fetch vs. node-fetch Library

Node.js versions 18 and above ship with a built-in global fetch() function, powered by Undici—a high-performance HTTP client library developed by the Node.js project. This native implementation offers several advantages:

  • Zero dependencies – No additional installation required
  • Performance – Built on Undici’s optimized HTTP stack
  • Future compatibility – Aligned with the evolving WHATWG specification

However, the node-fetch library remains relevant for specific scenarios:

  • Legacy Node.js versions – Projects running on Node.js 16 or earlier
  • Specific features – Certain edge cases where native fetch behavior differs
  • Migration flexibility – Gradual transition paths for existing codebases

The key distinction for proxy integration is that native Fetch and node-fetch use different agent interfaces. node-fetch accepts an agent option compatible with the Node.js http.Agent API, while native Fetch (powered by Undici) requires a dispatcher option that accepts Undici’s Dispatcher interface.

How Fetch Handles Requests and Responses

The fetch() function accepts a URL and an optional options object, returning a Promise that resolves to a Response object. Understanding this flow is essential for effective proxy integration:

const response = await fetch('https://api.example.com/data');
const data = await response.json();

Key characteristics of the Fetch API:

Promise-based resolution – The Promise resolves as soon as response headers arrive, not when the full body has downloaded. The response body is exposed as a ReadableStream, enabling efficient processing of large payloads.

Response body methods – The Response object provides multiple methods for consuming the body: response.text() for plain text or HTML, response.json() for JSON data, and response.buffer() for binary data.

Error handling – Unlike older approaches, Fetch does not reject the Promise for HTTP error status codes (404, 500, etc.). Only network-level failures—DNS errors, connection refusals, or aborted requests—trigger rejection. Developers must explicitly check response.ok or response.status.

Proxy Integration: The Core Challenge

Why Proxies Are Necessary

Proxy servers serve as intermediaries between client applications and destination servers. In the context of Node.js applications, proxies address several common requirements:

Corporate Network Compliance – Enterprise environments frequently mandate that all outbound traffic pass through corporate proxy servers for security monitoring, access control, and traffic logging. Applications deployed in these environments must be proxy-aware to function correctly.

Geographic Access – APIs and web services often implement geographic restrictions based on the originating IP address. Routing requests through proxy servers in specific regions enables access to region-locked content.

Rate Limiting and IP Reputation – Web scraping and data collection operations that send high volumes of requests from a single IP address frequently trigger rate limiting or IP blocking. Distributing requests across a pool of proxy IP addresses mitigates this risk.

Privacy and Anonymity – Proxy servers can obscure the originating IP address, providing an additional layer of privacy for sensitive operations.

Why Fetch Does Not Support Proxies Natively

A critical technical detail: neither node-fetch nor Node.js native Fetch supports proxies natively. This design choice stems from the separation of concerns between the Fetch API (which handles request/response semantics) and the underlying network layer (which handles connection establishment and routing).

For node-fetch, the library uses Node.js’s http and https modules for network transport. These modules support custom agents that can be configured with proxy settings. However, node-fetch itself does not provide built-in proxy configuration—developers must supply a custom agent.

For native Fetch, the situation is more complex. Native Fetch is powered by Undici, which uses a Dispatcher interface rather than the traditional Agent interface. While Undici provides proxy-capable dispatchers, the global fetch() function does not automatically apply them.

This architectural decision means that proxy integration requires explicit configuration regardless of whether developers use node-fetch or native Fetch.

Solution 1: Environment Variable Configuration (Node.js 22.21+)

The NODE_USE_ENV_PROXY Feature

Node.js 22.21.0 and 24.0.0 introduced built-in proxy support for the native fetch() function through the NODE_USE_ENV_PROXY environment variable. When enabled, Node.js parses standard proxy environment variables and routes HTTP and HTTPS requests through the specified proxy.

This feature represents a significant simplification for developers working in corporate environments or any scenario requiring consistent proxy configuration across all requests.

Configuration Steps

Enable the feature – Set NODE_USE_ENV_PROXY=1 in your environment:

export NODE_USE_ENV_PROXY=1

Set proxy environment variables – Configure the standard proxy variables:

export HTTP_PROXY=http://proxy.company.com:8080
export HTTPS_PROXY=http://proxy.company.com:8080
export NO_PROXY=localhost,127.0.0.1,.company.com

Run your application – Execute your Node.js script with the configuration active:

node app.js

Alternatively, use the command-line flag --use-env-proxy without setting the environment variable explicitly:

node --use-env-proxy app.js

Supported Node.js Versions

Feature Minimum Version
fetch() proxy support via NODE_USE_ENV_PROXY Node.js 22.21.0 or 24.0.0+
http/https request proxy support Node.js 22.21.0 or 24.5.0+

Limitations and Considerations

The environment variable approach offers simplicity but has limitations:

Global scope – Proxy configuration applies to all requests, which may not be appropriate for applications that need different proxies for different destinations.

No per-request granularity – Individual requests cannot use different proxies without overriding the global configuration.

Proxy authentication – Basic authentication can be included in the proxy URL (e.g., http://user:pass@proxy.company.com:8080), but more complex authentication may require alternative approaches.

Version requirements – This feature is only available in recent Node.js versions, limiting its use in legacy environments.

Code Example

// When NODE_USE_ENV_PROXY=1 and HTTP_PROXY/HTTPS_PROXY are set,
// all fetch() calls route through the configured proxy automatically

async function fetchData() {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
}

fetchData().catch(console.error);

Solution 2: Custom Agent with node-fetch

Understanding the Agent Pattern

For projects using the node-fetch library, proxy integration requires creating a custom agent and passing it via the agent option. The https-proxy-agent package provides the necessary agent implementation.

This approach offers fine-grained control over proxy configuration, enabling different proxies for different requests and compatibility with older Node.js versions.

Installation

Install the required dependencies:

npm install node-fetch https-proxy-agent

For Node.js 18+ where native fetch is available, node-fetch is optional if you prefer native Fetch (though native Fetch does not support the agent option).

Basic Implementation

import fetch from 'node-fetch';
import { HttpsProxyAgent } from 'https-proxy-agent';

// Create the proxy agent
const proxyAgent = new HttpsProxyAgent('http://proxy.example.com:8080');

// Use the agent in the fetch request
const response = await fetch('https://api.example.com/data', {
    agent: proxyAgent
});

const data = await response.json();
console.log(data);

Proxy Authentication

When the proxy server requires authentication, credentials can be included in the proxy URL:

const proxyAgent = new HttpsProxyAgent(
    'https://username:password@proxy.example.com:8080'
);

SOCKS5 Proxy Support

The https-proxy-agent package supports SOCKS5 proxies through the socks-proxy-agent package:

npm install socks-proxy-agent
import { SocksProxyAgent } from 'socks-proxy-agent';

const proxyAgent = new SocksProxyAgent('socks5://user:pass@proxy.example.com:1080');

const response = await fetch('https://api.example.com/data', {
    agent: proxyAgent
});

Session Reuse

For web scraping scenarios involving multiple requests within a session, reusing the same agent maintains consistent IP addresses across requests:

const proxyAgent = new HttpsProxyAgent('http://proxy.example.com:8080');

// First request
const response1 = await fetch('https://example.com/page1', { agent: proxyAgent });

// Second request - same proxy IP
const response2 = await fetch('https://example.com/page2', { agent: proxyAgent });

Environment Variable Integration

For production deployments, store proxy credentials in environment variables rather than hardcoding them:

import fetch from 'node-fetch';
import { HttpsProxyAgent } from 'https-proxy-agent';
import dotenv from 'dotenv';

dotenv.config();

const proxyAgent = new HttpsProxyAgent(process.env.HTTP_PROXY);

const response = await fetch('https://api.example.com/data', {
    agent: proxyAgent
});

Solution 3: Undici ProxyAgent for Native Fetch

The Undici Dispatcher Model

Native Node.js Fetch is powered by Undici, which uses a Dispatcher interface for managing HTTP connections. To route native Fetch requests through a proxy, developers must import the ProxyAgent from Undici and pass it via the dispatcher option.

This approach is the equivalent of the custom agent method for native Fetch. It works with Node.js versions that support native Fetch (18+) and offers the same granular control as the node-fetch approach.

Basic Implementation

import { ProxyAgent } from 'undici';

const proxyAgent = new ProxyAgent('http://proxy.example.com:8080');

const response = await fetch('https://api.example.com/data', {
    dispatcher: proxyAgent
});

const data = await response.json();
console.log(data);

Authentication

Proxy authentication is handled through the proxy URL:

const proxyAgent = new ProxyAgent('http://user:pass@proxy.example.com:8080');

Environment Variable Agent

Undici provides EnvHttpProxyAgent, which reads proxy configuration from environment variables:

import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';

// Set environment variables: HTTP_PROXY, HTTPS_PROXY, NO_PROXY
const proxyAgent = new EnvHttpProxyAgent();

// Apply to all fetch() calls globally
setGlobalDispatcher(proxyAgent);

// Now all fetch() calls use the proxy
const response = await fetch('https://api.example.com/data');

Global Dispatcher Configuration

For applications where all requests should use the same proxy, setting the global dispatcher is the most efficient approach:

import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';

if (process.env.HTTPS_PROXY || process.env.HTTP_PROXY) {
    setGlobalDispatcher(new EnvHttpProxyAgent());
}

// All fetch() calls now route through the proxy
async function fetchData() {
    const response = await fetch('https://api.example.com/data');
    return response.json();
}

Important Compatibility Notes

Node.js version – This approach requires Node.js 18+ for native Fetch support.

Import method – Undici must be imported directly. The global fetch() function does not automatically expose Undici’s dispatcher options.

ProxyAgent vs. HttpsProxyAgentProxyAgent from Undici is the equivalent of HttpsProxyAgent from the https-proxy-agent package. They are not interchangeable.

Solution 4: global-agent Package

Overview

The global-agent package provides an alternative approach to proxy configuration, enabling proxy support with minimal code changes. It works by monkey-patching Node.js’s global HTTP/HTTPS agents, affecting all requests made through http.request, https.request, and libraries built on them.

Installation and Configuration

npm install global-agent
import 'global-agent/bootstrap';

// Set environment variables
process.env.GLOBAL_AGENT_HTTP_PROXY = 'http://proxy.example.com:8080';
process.env.GLOBAL_AGENT_HTTPS_PROXY = 'http://proxy.example.com:8080';
process.env.GLOBAL_AGENT_NO_PROXY = 'localhost,127.0.0.1';

Usage with node-fetch

import 'global-agent/bootstrap';
import fetch from 'node-fetch';

// The global-agent bootstrap makes node-fetch use the proxy automatically
const response = await fetch('https://api.example.com/data');
const data = await response.json();

Advantages and Limitations

Advantages:

  • Minimal code changes required
  • Works with multiple HTTP libraries
  • Environment variable-based configuration

Limitations:

  • Less granular control than per-request agents
  • May not work with all HTTP client implementations
  • Potential compatibility issues with certain proxy types

Proxy Selection for Node.js Applications

Residential vs. Datacenter Proxies

When integrating proxies into Node.js applications, the type of proxy selected significantly impacts success rates, particularly for web scraping and data collection operations.

Residential Proxies – Sourced from real residential networks, these IP addresses appear to originate from legitimate home internet connections. They carry higher trust scores and are less likely to trigger blocking mechanisms on destination platforms. For applications requiring sustained access to CAPTCHA-protected or heavily monitored sites, residential proxies offer clear advantages.

Datacenter Proxies – Sourced from commercial data centers, these IP addresses are easier to identify and sometimes block. However, they offer higher bandwidth and lower latency, making them suitable for applications where IP authenticity is less critical than performance.

IPFLY’s dynamic residential proxies provide access to over 90 million residential IP addresses across 190+ countries. The rotation capabilities enable Node.js applications to distribute requests across diverse IPs, maintaining low request-per-IP ratios that stay below detection thresholds. With average response times of 0.6 seconds and 99.9% availability, the infrastructure supports high-volume automated workflows without introducing latency.

For applications requiring consistent IP assignments—such as session-based workflows or authenticated API access—IPFLY’s static residential proxies provide 100% exclusive, ISP-registered residential IP addresses that remain stable over time. The protocol support for HTTP/HTTPS and SOCKS5 ensures compatibility with all proxy integration methods discussed in this guide.

IPFLY’s datacenter proxies deliver 99.9% availability with global coverage across major regions, providing the bandwidth and reliability required for high-performance applications where residential IPs are not required.

Proxy Protocol Selection

The proxy protocol must align with both the proxy provider’s capabilities and the Node.js application’s requirements:

HTTP/HTTPS Proxies – Support HTTP and HTTPS traffic. HTTPS proxies handle encrypted traffic and are essential for secure API communication. Most proxy integration methods support HTTP/HTTPS proxies by default.

SOCKS5 Proxies – Operate at a lower level, handling any TCP-based traffic regardless of application protocol. SOCKS5 proxies offer greater flexibility for applications that use non-HTTP protocols.

Both https-proxy-agent and Undici’s ProxyAgent support SOCKS5 proxies through appropriate configuration.

Proxy Rotation Strategies

For web scraping and data collection applications, implementing proxy rotation is essential to maintain sustained access:

Request-level rotation – Each request uses a different proxy IP from a pool. This approach maximizes IP diversity but may be inappropriate for session-based workflows.

Session-level rotation – A single proxy IP is used for the duration of a session, with rotation occurring between sessions. This approach maintains consistency for workflows that require stable IP-account pairing.

Health-based rotation – Proxy IPs are rotated when they exhibit poor performance, high latency, or blocking. This approach optimizes for reliability and success rates.

For Node.js applications implementing rotation, the agent creation can be wrapped in a function that selects from a pool of proxy URLs:

import { HttpsProxyAgent } from 'https-proxy-agent';

const proxyPool = [
    'http://proxy1.example.com:8080',
    'http://proxy2.example.com:8080',
    'http://proxy3.example.com:8080'
];

function getProxyAgent() {
    const proxy = proxyPool[Math.floor(Math.random() * proxyPool.length)];
    return new HttpsProxyAgent(proxy);
}

// Use in requests
const response = await fetch('https://api.example.com/data', {
    agent: getProxyAgent()
});

Best Practices for Node.js Proxy Integration

Error Handling and Retry Logic

Network requests through proxies introduce additional failure modes. Implementing robust error handling and retry logic is essential for production applications:

async function fetchWithRetry(url, options, maxRetries = 3) {
    for (let i = 0; i < maxRetries; i++) {
        try {
            const response = await fetch(url, options);
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
            return response;
        } catch (error) {
            console.warn(`Attempt ${i + 1} failed:`, error.message);
            if (i === maxRetries - 1) throw error;
            // Exponential backoff
            await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
        }
    }
}

TLS and Certificate Validation

Corporate proxies often use self-signed certificates or certificate authorities that are not in Node.js’s default trust store. Common error: UNABLE_TO_VERIFY_LEAF_SIGNATURE.

Solution 1 – Add the corporate CA certificate to Node.js’s trust store using the NODE_EXTRA_CA_CERTS environment variable.

Solution 2 – Configure the agent to use custom CA certificates:

import { HttpsProxyAgent } from 'https-proxy-agent';
import fs from 'fs';

const ca = fs.readFileSync('/path/to/corporate-ca.pem');

const proxyAgent = new HttpsProxyAgent({
    host: 'proxy.company.com',
    port: 8080,
    ca: ca
});

Important: Disabling certificate validation (rejectUnauthorized: false) is strongly discouraged for production applications due to security risks.

Connection Pooling and Performance

For high-volume applications, reusing proxy agents (rather than creating new ones for each request) improves performance by maintaining persistent connections:

// Create once, reuse many times
const proxyAgent = new HttpsProxyAgent('http://proxy.example.com:8080');

// Reuse for multiple requests
const results = await Promise.all([
    fetch(url1, { agent: proxyAgent }),
    fetch(url2, { agent: proxyAgent }),
    fetch(url3, { agent: proxyAgent })
]);

Environment-Based Configuration

For applications deployed across different environments (development, staging, production), environment-based proxy configuration simplifies management:

import { HttpsProxyAgent } from 'https-proxy-agent';

const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;

const getFetchOptions = () => {
    if (proxyUrl) {
        return { agent: new HttpsProxyAgent(proxyUrl) };
    }
    return {};
};

// Use in requests
const response = await fetch('https://api.example.com/data', getFetchOptions());
Node Fetch: A Technical Guide to HTTP Requests and Proxy Integration

The Fetch API in Node.js provides a modern, promise-based approach to HTTP requests, but its proxy integration requires explicit configuration. The appropriate solution depends on the Node.js version, the specific requirements of the application, and the proxy infrastructure available.

For applications running on Node.js 22.21+ or 24.0+, the NODE_USE_ENV_PROXY environment variable offers the simplest configuration, routing all requests through the specified proxy with minimal code changes. This approach is ideal for corporate environments where consistent proxy configuration is required across all requests.

For applications requiring fine-grained control, the custom agent approach with https-proxy-agent and node-fetch provides per-request proxy selection, authentication flexibility, and compatibility with older Node.js versions. This approach is well-suited for web scraping, data collection, and applications that route different requests through different proxies.

For native Fetch users, Undici’s ProxyAgent and EnvHttpProxyAgent offer equivalent capabilities through the dispatcher interface. The global dispatcher configuration provides the same simplicity as the environment variable approach while maintaining per-request granularity when needed.

The global-agent package offers a middle ground, enabling proxy support with minimal code changes through environment-based configuration that works across multiple HTTP libraries.

Selecting the right proxy infrastructure is equally important. Residential proxies provide higher trust scores and reduced blocking risk, while datacenter proxies offer higher performance and lower latency. The choice depends on the specific requirements of the application and the sensitivity of the destination platforms.

Node Fetch: A Technical Guide to HTTP Requests and Proxy Integration

For Node.js applications requiring reliable proxy infrastructure for HTTP requests, web scraping, or API integration, IPFLY provides professional proxy solutions designed for performance, reliability, and compatibility:

  • Dynamic Residential Proxies – Access over 90 million residential IP addresses across 190+ countries with low-latency performance, enabling effective IP rotation for Node.js applications.
  • Static Residential Proxies – Exclusive, persistent residential IP addresses for consistent access patterns and session-based workflows.
  • Datacenter Proxies – High-performance proxy infrastructure with 99.9% availability for bandwidth-intensive applications.

Build your Node.js proxy infrastructure today. Visit IPFLY’s homepage to explore the full range of proxy solutions, or register now for immediate access to professional proxy capabilities that support your development and data collection needs.