Magic Tools
Developer ToolsBy CooconAugust 5, 2026144 views4 min read

Node.js Built-in Fetch Doesn't Use HTTPS_PROXY: The Second Pitfall When Connecting to Google API

1. Problem Description

In the previous article 'GA4 Data API Proxy Environment Timeout DEADLINE_EXCEEDED', I fixed the gRPC proxy. Following the same environment variable approach, I wrote a data fetching script for Google Search Console—GSC is a regular REST API, so using the built-in fetch is sufficient:

const res = await fetch(
  "https://searchconsole.googleapis.com/webmasters/v3/sites",
  { headers: { Authorization: `Bearer ${token}` } }
);

The performance when running:

TypeError: fetch failed
  cause: ConnectTimeoutError: Connect Timeout Error

In the same terminal:

  • curl -x http://127.0.0.1:7897 https://searchconsole.googleapis.com works normally
  • HTTPS_PROXY is exported in both uppercase and lowercase
  • The grpc_proxy fixed for GA4 in the previous article is still there

Environment variables are all set, but fetch doesn't read any of them.

2. Environment

Item Details
OS macOS (Darwin 23.6)
Node.js 22.18.0 (built-in fetch provided by undici)
Target API Google Search Console (REST)
Proxy Local Clash mixed port 127.0.0.1:7897

3. Root Cause: This Is Not a Bug, It's a Design

The built-in fetch in Node.js from v18 onwards is based on undici. The default behavior of undici is: completely ignore environment variables like HTTP_PROXY / HTTPS_PROXY / NO_PROXY, and if you need a proxy, you must explicitly configure the dispatcher in the code.

This is contrary to most people's intuition, because almost all common tools read these variables:

Tool / Library Reads Environment Proxy Variables?
curl / wget ✅ Yes
axios ✅ Yes
gaxios (HTTP layer of the googleapis ecosystem) ✅ Reads uppercase HTTPS_PROXY and lowercase https_proxy
request / node-fetch (agent libraries) Generally supported by ecosystem solutions
Node built-in fetch (undici) ❌ No by default

The typical pitfall path is: first use curl to verify that the proxy is fine, then write the fetch version, and then question your life over the timeout—the success of curl is precisely the misleading factor, as it and fetch are not in the same proxy discovery mechanism.

4. Three Fix Paths

If your scenario is similar to mine, calling Google APIs, the smoothest solution is to let the authentication library handle the HTTP layer as well:

import { GoogleAuth } from "google-auth-library";

const auth = new GoogleAuth({
  scopes: ["https://www.googleapis.com/auth/webmasters.readonly"],
});
const client = await auth.getClient();

// client.request uses gaxios under the hood, which honors HTTPS_PROXY
const res = await client.request({
  url: "https://searchconsole.googleapis.com/webmasters/v3/sites",
});

This solution has two additional benefits:

  • Token refresh is automatically handled, no need to manually construct the Authorization header
  • If you are already using official SDKs like @google-analytics/data, google-auth-library is their transitive dependency, no additional installation is needed, and it also avoids the large googleapis package

gaxios has the same strict requirements for proxy addresses as grpc-js—the value must have the http:// prefix; writing 127.0.0.1:7897 directly will throw Invalid URL.

Path 2 (General): undici ProxyAgent

For scenarios where you want to use fetch but not with Google APIs, explicitly configure the dispatcher for undici:

import { ProxyAgent, setGlobalDispatcher } from "undici";

setGlobalDispatcher(new ProxyAgent("http://127.0.0.1:7897"));

// every fetch in this process now goes through the proxy
const res = await fetch("https://example.com");

If you don't want to affect globally, pass it per request:

const res = await fetch(url, {
  dispatcher: new ProxyAgent("http://127.0.0.1:7897"),
});

(dispatcher is an undici extended option, and type assertions may be needed in TypeScript.)

Path 3 (Newer Node): Experimental Environment Variable Switch

Newer versions of Node.js provide an experimental NODE_USE_ENV_PROXY=1 to make the built-in fetch respect environment proxy variables. If your Node version supports this and you can accept experimental features, this is the solution with minimal changes—but in projects locked to Node 22 (like mine), the first two paths are more stable. Test it on the target version before use, and don't write experimental switches into production scripts.

5. Combined with the Previous Article: Quick Reference for Google API Proxy Configuration

With the same Google credentials, the transport layers of different APIs are completely different, so the proxy configurations are also completely different:

API Transport Layer Proxy Configuration
GA4 Data API (@google-analytics/data) gRPC Lowercase grpc_proxy=http://... (uppercase HTTPS_PROXY is ineffective)
Search Console / most googleapis REST (gaxios) HTTPS_PROXY=http://... (recognized in both cases)
Writing fetch yourself undici All environment variables are ineffective; must configure ProxyAgent in code

My actual approach is to uniformly prefix the script with two variables to cover the first two cases:

HTTPS_PROXY=http://127.0.0.1:7897 grpc_proxy=http://127.0.0.1:7897 node script.js

6. Troubleshooting Mantras

  • curl works ≠ your code will work: curl has its own proxy discovery mechanism, which cannot verify the behavior of the HTTP library you use
  • When changing libraries, first ask: Does this library read environment proxy variables? Which one? What case?—directly browse the source code in node_modules; it's faster than checking documentation and won't be outdated
  • When Node built-in fetch times out and the environment has a proxy, the first reaction should be that undici doesn't read environment variables; don't waste time checking DNS and firewalls

Related Articles

Dev Breakfast · 2026-09-19

Today's headline: The Internal Repo Earned by a Single HEIC Image: OpenAI Forum's SSO Vulnerability. Plus 4 more: ZCode Silently Uploads 313MB Encrypted Package, Keys Only on the Server; Telstra Outage for a Night: One GPS Receiver Sent the National Network Back to 2006; and more.

daily-intelSep 19, 202610 min
23

Dev Breakfast · 2026-09-18

Today's headline: AWS says some data in Middle East facilities can't be recovered: backup is harder than you think. Plus 7 more: Nvidia allows Rust to directly write GPU kernels, with two paths in parallel; 4B model-generated query plans are 81% faster than Postgres; and more.

daily-intelSep 18, 20269 min
48

Service Up, Ports Open, Certs Valid, VPN Dead for 4 Hours: Tailscale Took Over DNS and Left the Proxy Box With No Upstream

A Los Angeles VPS running sing-box (VLESS-REALITY + Hysteria2) lost its VPN the day after Tailscale was installed. systemctl, ports and certificates were all fine. The root cause was in /etc/resolv.conf: Tailscale manages DNS by default, the tailnet had no global nameservers, and when dhclient renewed its lease tailscaled read an empty resolv.conf and dropped its upstream list. From then on every public domain got SERVFAIL, and the REALITY handshake could not even resolve www.apple.com. Full timeline, the evidence for each step, three fixes, and the rules we added to CLAUDE.md so an AI assistant (Claude Code) does not walk into this again.

claude-codetroubleshooting+8
pitfallsSep 17, 20266 min
48

Dev Breakfast · 2026-09-17

Today's headline: Firefox 156 pushes 'Suggest' ads in the address bar, PDF starts up 45% faster. Plus 7 more: Karpathy's autoresearch six months later: Shopify uses it to improve 40+ metrics, rekursiv refreshes nanochat record in three days; Replacing actions/setup-go: Golang CI scaling actual test; and more.

daily-intelSep 17, 20266 min
75

Published by Magic Tools