Magic Tools
Developer ToolsBy CooconAugust 5, 2026338 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

Blocking rm -rf in Claude Code: A Deny Rule Stopped 14 Deletes in Two Weeks, 7 Other Spellings Still Get Through, and a Hook That Closes the Gap

To keep Claude Code from deleting things by accident, I added a deny rule, Bash(rm -rf *), to my global settings. Across my sessions from October 7 to 21 it blocked 14 deletes. After each denial the model picked a new directory name 8 times and dropped the delete 6 times. It never tried another spelling. Then I replayed 22 delete commands verbatim against Claude Code 2.1.285 through a stub. rm -fr, rm -r -f, rm -Rf and rm -r deleted the directory in manual, auto and bypass mode alike. /bin/rm, find -delete and bash -c deleted it in manual and bypass mode. In auto mode, rm -fr inside the working directory skips the classifier because it 'would be allowed in acceptEdits mode'. A second, built-in dangerous-rm check fires on targets like $D/*, even in bypass mode, but loses track of a variable assignment when a heredoc comes earlier in the same command. The post ends with a PreToolUse hook that blocks every recursive spelling in all three modes and tells the model what to do instead.

claude-codehooks+5
pitfallsOct 21, 20268 min
445

The Self-Reinforcing Flywheel Is Harder Than It Looks: 40 Days of an LLM Auto-Tuning Loop

We ran an 'LLM proposes, code decides' auto-tuning loop on a daily WeChat newsletter for 40 days: 18 versions, 8 rollbacks. Three of those rollbacks overturned decisions a human had made, and one version whose knobs were identical to its parent still got rolled back. Using our real numbers, telling apart a 20% difference needs about 25,000 reads per version. At 60 reads a day, that is more than 400 days. This postmortem covers five reasons the flywheel never turned, and what actually moved the numbers.

wechatpostmortem+4
pitfallsOct 21, 20268 min
293

Dev Breakfast · 2026-10-21

Today's headline: A single "bonsoir, Elliot" makes the model leak its .env: a backdoor for under $50. Plus 4 more: DeepSeek open-sources its inference engine: V4.1 Flash only, and KV cache still missing; TorchTPU goes open source: PyTorch models can run directly on TPU; and more.

daily-intelOct 21, 20268 min
641

Dev Breakfast · 2026-10-20

Today's headline: Should You Merge AI Code You Don't Understand: Settle the Abstraction Layer Yourself First. Plus 4 more: tmux as a Remote Control: Multi-Session Claude Code and send-keys; headroom Goes Open Source: 95% Token Savings on JSON, Only 20% for Coding Agents; and more.

daily-intelOct 20, 20265 min
574

Published by Magic Tools