Proxying fetch requests in server-side JavaScript

If you retrieve content from the internet, you’ll eventually need to proxy requests. Proxying is useful for logging traffic, modifying headers, altering content, improving performance through caching, or hiding an originating IP address. Most server-side JavaScript runtimes allow you to proxy requests using fetch().

The Fetch standard1 doesn’t specify request proxying because it’s a browser-focused API. Consequently, server-side runtimes implement proxying differently.

Node.js

Node.js natively supports proxying fetch() requests through environment variables as of Node.js v22.21.0 and v24.5.02. You can set the HTTP_PROXY and HTTPS_PROXY environment variables to specify the proxy server for HTTP and HTTPS requests, respectively:

# Enable Node.js to use environment variables for proxying
export NODE_USE_ENV_PROXY=1

export HTTP_PROXY=http://username:[email protected]:8080
export HTTPS_PROXY=https://username:[email protected]:8080

The NODE_USE_ENV_PROXY variable is required to enable this behavior, as it is disabled by default to avoid unintended proxying. You can also use the --use-env-proxy flag when running your Node.js application to enable this feature without setting the environment variable:

node --use-env-proxy your-app.js

You can also proxy specific requests programmatically. While the Node.js fetch() API doesn’t natively support proxying, it’s built on top of the undici package3, which does. Since Node.js doesn’t expose the ProxyAgent class4 directly, you’ll first need to install undici:

npm i undici

To use a proxy, create a ProxyAgent instance and pass it as the dispatcher option:

import { ProxyAgent } from 'undici';

const agent = new ProxyAgent('http://username:[email protected]:8080');

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

const body = await response.json();

Deno

Deno’s fetch() uses the client property in the options object to specify a proxy client:

const client = Deno.createHttpClient({
  proxy: { url: "http://username:[email protected]:8080" },
});

const response = await fetch("https://api.example.com", { client });
const data = await response.json();

client.close(); // Remember to close the client when done

Despite its name, Deno.createHttpClient() also supports HTTPS proxies:

const client = Deno.createHttpClient({
  proxy: { url: "https://username:[email protected]:8080" },
});

const response = await fetch("https://api.example.com", { client });
const data = await response.json();

client.close(); // Remember to close the client when done

Bun

Bun provides native support for fetch() proxying via the proxy property:

const response = await fetch("https://api.example.com", {
  proxy: "http://username:[email protected]:8080"
});

const body = await response.json();

Cloudflare Workers

The Cloudflare Workers runtime does not natively support proxying fetch() requests through environment variables or programmatic options. However, you can work around this by using a Node.js Docker container to handle the requests.

The @humanwhocodes/proxy-fetch-server5 package is a small utility I created to simplify this. Here’s how to run it in a container:

FROM node:22-slim

WORKDIR /app
RUN npm install -g @humanwhocodes/proxy-fetch-server@2

EXPOSE 8080
ENV PORT=8080

CMD ["npx", "@humanwhocodes/proxy-fetch-server"]

Next, define a container class:

import { Container } from "@cloudflare/containers";

export class ProxyFetchContainer extends Container {
    defaultPort = 8080;
}

Update your wrangler.jsonc file to include the binding:

{
    "name": "proxy-fetcher",

    "containers": [
        {
            "class_name": "ProxyFetchContainer",
            "image": "./Dockerfile"
        }
    ],
    "durable_objects": {
        "bindings": [
            {
                "name": "PROXY_FETCH_CONTAINER",
                "class_name": "ProxyFetchContainer"
            }
        ]
    },
    "migrations": [
        {
            "tag": "v1",
            "new_sqlite_classes": [
                "ProxyFetchContainer"
            ]
        }
    ],
}

Finally, access the proxy container in your code:

// get and start container
const container = env.PROXY_FETCH_CONTAINER.getByName("proxy-fetch-server");

await container.startAndWaitForPorts({
    startOptions: {
        envVars: {
            FETCH_PROXY: "http://username:[email protected]:8080"
        }
    }
});

// Make request to the container
const containerResponse = await container.fetch(
    new Request("http://container/", {
        method: "POST",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify({ url })
    })
);

While this requires more setup than other runtimes, it’s a reliable solution for this use case.

Conclusion

Proxying fetch() requests is a common requirement in server-side development, whether for security, monitoring, or performance. While runtimes like Deno and Bun offer straightforward programmatic APIs, others like Node.js and Cloudflare Workers require a bit more legwork. Regardless of your choice, understanding these patterns ensures your applications can communicate reliably with the outside world. Give these methods a try in your next project; you’ll find that handling proxies is just another essential tool in your server-side JavaScript toolkit.

Update(2026-03-04): Added information about Node.js support for proxy-related environment variables.

Footnotes

  1. Fetch Standard

  2. Node.js Enterprise Network Configuration

  3. Undici

  4. Issue #43187: Expose Undici ProxyAgent

  5. @humanwhocodes/proxy-fetch-server

Master Your Time as a Tech Lead

Free E-book - Managing Your Interrupt Rate

Take Control of Your Calendar

  • Understanding interrupt patterns
  • Strategies for time management
  • Communication techniques
  • Productivity optimization

The popular blog post series plus frequently asked questions, all in one convenient PDF.

Download Your Free Copy

Get immediate access to proven strategies for managing interruptions.