Skip to main content

5 posts tagged with "WSL2"

View all tags

VSCode WSL Extension 'Failed to Fetch'? VSIX Download Saved as Gzip Instead of Zip

ยท 4 min read

After a major VSCode upgrade, the Claude Code extension becomes unresponsive for extended periods. The only option is to uninstall and reinstall โ€” but then you hit Failed to fetch, VSIX format errors, and a series of other installation issues.

TL;DRโ€‹

After a VSCode upgrade, the extension may become unresponsive. Uninstalling and reinstalling reports Failed to fetch, and manually downloading the VSIX hits format errors. Root cause: the marketplace server returns gzip-compressed content (standard HTTP content negotiation), and curl -L may not auto-decompress when following redirects โ€” the saved file ends up as gzip instead of zip. Fix: manual curl download โ†’ gunzip decompress โ†’ code --install-extension.

Symptomsโ€‹

After upgrading VSCode:

  1. Claude Code conversations become unresponsive, stuck at Manifesting...
  2. Clicking the extension icon takes forever to open a dialog
  3. The only option is to uninstall and reinstall โ€” but every install method fails

After uninstalling, all reinstall attempts fail:

# VSCode UI install โ†’ Failed to fetch
# Command line install โ†’ same error
code --install-extension anthropic.claude-code
# Error installing extension: Failed to fetch

Root Causeโ€‹

Three factors combine to cause this issue:

VSCode upgrade triggers WSL extension host reinitialization. Extensions like Claude Code must be installed on the WSL side to run (defined as remote extension host extensions). After a major upgrade, the extension may become unresponsive and require uninstalling and reinstalling.

VSCode extension installation uses its own network stack, ignoring WSL's http_proxy. Even with a proxy configured in WSL (e.g., http://172.30.0.1:7897), VSCode's extension download channel uses its own networking, unaffected by system proxy variables.

Marketplace server returns gzip-compressed content, curl -L doesn't auto-decompress. The marketplace API endpoint returns gzip-compressed responses via HTTP content negotiation โ€” this is standard behavior. When curl -L follows redirects, it may not automatically decompress Content-Encoding: gzip, so the saved file ends up as gzip format instead of the expected zip format. Running file on the download shows gzip compressed data instead of Zip archive data.

Solutionโ€‹

1. Download VSIX manuallyโ€‹

Use curl in the WSL terminal:

curl -L "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/anthropic/vsextensions/claude-code/latest/vspackage" \
-o ~/claude-code.vsix

2. Check file formatโ€‹

file ~/claude-code.vsix

If the output is gzip compressed data, the file is gzip format โ€” proceed to decompress. If it says Zip archive data, you have the raw format and can skip the next step.

3. Decompress to restore zip formatโ€‹

gunzip -c ~/claude-code.vsix > ~/claude-code-real.vsix

Confirm the format is correct:

file ~/claude-code-real.vsix
# Output: Zip archive data, at least v2.0 to extract

4. Verify version (optional)โ€‹

unzip -p ~/claude-code-real.vsix extension/package.json | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['version'])"

5. Installโ€‹

code --install-extension ~/claude-code-real.vsix

After installation, reload the VSCode window (Ctrl+Shift+P โ†’ Reload Window) to restore the extension.

WSL2 environment issues like this are common โ€” if you've also encountered Docker Desktop host network mode ports unreachable from the host, it's similarly related to WSL2's unique architecture.

Important Notes

  • This fix works for any VSIX download that ends up as gzip format instead of zip, not just Claude Code
  • Manually installed extensions won't auto-update โ€” future VSCode upgrades may handle updates through the normal channel
  • Try adding the --compressed flag to curl to let it handle gzip decompression automatically; if that doesn't work, fall back to the manual gunzip workflow

FAQโ€‹

Is the VSCode extension "Failed to fetch" error caused by a proxy?โ€‹

Not necessarily. The marketplace server itself returns gzip-compressed content via standard HTTP content negotiation, and curl -L may not auto-decompress when following redirects. Manual download and gunzip decompression before installing bypasses this.

What to do when WSL extensions become unresponsive after a VSCode upgrade?โ€‹

Major VSCode upgrades reinitialize the WSL remote extension host, which may cause extensions to become unresponsive. Uninstall, download the VSIX manually, and install with code --install-extension to restore it.

VSIX install reports "not a zip file" โ€” what now?โ€‹

The file is in gzip format instead of zip. The marketplace server returns gzip-compressed content, and curl -L may not auto-decompress. Decompress with gunzip -c file.vsix.gz > file.vsix before installing, and use the file command to verify the format.

CCLEE

Independent developer, 24 years in e-commerce, focused on grounding AI in real business scenarios.

Work with me

Node.js fetch ignores proxy env vars? undici doesn't read http_proxy

ยท 4 min read

In a WSL2 environment with https_proxy properly set, Node.js fetch() still times out when accessing external URLs.

Encountered this issue while building an AI-powered e-commerce tool for a client. Here's the root cause and solution.

TL;DRโ€‹

Node.js 22+ built-in fetch() is powered by undici, which by design does not read http_proxy/https_proxy environment variables. Solution: install node-fetch@3 + https-proxy-agent, create a proxy-aware fetch instance. Falls back to direct connection when no proxy is configured in production.

Problemโ€‹

WSL2 environment with https_proxy correctly set. curl works fine:

echo $https_proxy
# http://172.30.224.1:7897

curl -I https://httpbin.org/ip
# HTTP/1.1 200 OK

But Node.js fetch() times out:

await fetch('https://httpbin.org/ip');
// FetchError: fetch failed
// cause: TimeoutError: Headers Timeout Error

If you're also seeing WSL2 proxy completely unreachable (even curl fails), check your firewall settings first.

Root Causeโ€‹

Node.js v22+ global fetch() is provided by the built-in undici 7.x. undici intentionally does not read http_proxy/https_proxy environment variables โ€” this is by design, not a bug.

Proxy behavior across different HTTP clients:

ClientReads env varsUses proxy
curlAuto-reads https_proxyโœ…
Node.js http/https modulesDoes not readโŒ
axios / node-fetch@3Reads https_proxyโœ…
Node.js built-in fetch() (undici)Does not readโŒ

This causes fetch() to time out in environments that require a proxy to access external networks (WSL2, corporate networks).

Solutionโ€‹

Install node-fetch@3 and https-proxy-agent:

npm install node-fetch@3 https-proxy-agent

Create a proxy-aware fetch instance:

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

const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
const agent = proxyUrl ? new HttpsProxyAgent(proxyUrl) : undefined;

export async function fetchWithProxy(url, options = {}) {
return fetch(url, { ...options, agent });
}

Usage is almost identical to the native fetch():

// Before
const res = await fetch('https://httpbin.org/ip');

// After
const res = await fetchWithProxy('https://httpbin.org/ip');

Why node-fetch instead of undici's ProxyAgent?โ€‹

Node.js v24 ships with undici 7.x, but the npm [email protected] ProxyAgent is incompatible with the built-in version:

import { ProxyAgent, setGlobalDispatcher } from 'undici';

// Node v24 error: UND_ERR_INVALID_ARG
// npm undici@8 ProxyAgent is incompatible with built-in undici@7 setGlobalDispatcher
setGlobalDispatcher(new ProxyAgent(proxyUrl));

node-fetch@3 + https-proxy-agent is version-agnostic with no compatibility issues. In production without a proxy, agent is undefined and it connects directly.

Caveats

  • Don't try to override global fetch with setGlobalDispatcher โ€” changes don't propagate to worker modules under tsx watch hot reload
  • npm [email protected] FormData types are incompatible with the global FormData, mixing them causes TypeScript compilation errors
  • node-fetch@3 is ESM-only, use import โ€” no require() support

FAQโ€‹

Why doesn't Node.js fetch read the http_proxy environment variable?โ€‹

Node.js 22+ built-in fetch is powered by undici, which by design does not read http_proxy/https_proxy environment variables. Use node-fetch or undici's ProxyAgent to configure proxy manually.

How to make Node.js fetch work through a proxy?โ€‹

Install node-fetch@3 and https-proxy-agent, then create a fetch instance with proxy support. When no proxy is configured in production, it falls back to direct connection, independent of Node version.

WSL2 has other networking pitfalls โ€” Docker Desktop's host mode also makes container ports unreachable from WSL2. The debugging approach is similar: verify curl connectivity first, then check application-level configuration.

Node version upgrades can introduce other issues too โ€” for example, JWT key format changes in Node 24. Worth checking when upgrading.

Need help with Node.js networking issues?

Get in touch

Puppeteer Blocked by Anti-Bot? From Chrome CDP to Electron Alternative

ยท 7 min read

Encountered this issue while building a data collection tool for a client. Here's the full troubleshooting journey from Puppeteer to Electron.

TL;DRโ€‹

Puppeteer stealth plugin cannot bypass advanced CAPTCHA-based anti-bot systems. Switching to Chrome CDP remote debugging led to WSL2 network isolation and Chrome single-instance issues. The final solution: use Electron BrowserWindow to load the target site, let the user log in manually, and automatically extract cookies via session.cookies.get() โ€” eliminating anti-bot detection and cross-platform problems entirely.

Scenario 1: Puppeteer Blocked by Anti-Botโ€‹

Problemโ€‹

Using puppeteer-extra + puppeteer-extra-plugin-stealth for automated login, the browser triggers CAPTCHA interception immediately after launch. Even after passing the CAPTCHA, subsequent pages detect the automation environment and force an exit.

import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';

puppeteer.use(StealthPlugin());

const browser = await puppeteer.launch({
headless: false,
args: [
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
'--disable-infobars',
],
});

const page = await browser.newPage();
await page.goto('https://target-site.com/login');
// Anti-bot CAPTCHA system detects automation, page is blocked

Root Causeโ€‹

Advanced anti-bot CAPTCHA systems don't just check basic fingerprints like navigator.webdriver. They detect automation through multiple dimensions: Chromium build signatures, Canvas/WebGL rendering differences, mouse trajectory patterns, and even DevTools Protocol call stacks. The stealth plugin fixes known fingerprint leaks but cannot eliminate the fundamental differences between Puppeteer's Chromium and a real Chrome browser.

After 8 rounds of debugging (removing timeouts, listening for disconnect events, investigating environment differences, fully configuring stealth plugin, etc.), we confirmed that no configuration could bypass the detection.

Solutionโ€‹

Abandoned Puppeteer automated login entirely. Switched to Chrome DevTools Protocol (CDP) to connect to the user's real browser and extract cookies.

Scenario 2: WSL2 Cannot Connect to Windows Chrome CDPโ€‹

Problemโ€‹

Running a Node.js script in WSL2, connecting to Windows Chrome's debugging port via chrome-remote-interface results in a connection timeout:

import CDP from 'chrome-remote-interface';

const client = await CDP({
host: 'localhost',
port: 9222,
});
// Error: connect ECONNREFUSED 127.0.0.1:9222

Chrome launched on Windows with:

chrome.exe --remote-debugging-port=9222

Running curl localhost:9222/json in Windows PowerShell works fine, but WSL2 cannot connect.

Root Causeโ€‹

Chrome's --remote-debugging-port=9222 binds to 127.0.0.1 by default โ€” the Windows loopback address. WSL2 and Windows have separate network stacks. localhost inside WSL2 points to the Linux loopback address, not Windows. So accessing localhost:9222 from WSL2 actually hits Linux's port 9222, not Windows Chrome.

Solutionโ€‹

Add --remote-debugging-address=0.0.0.0 when launching Chrome to make CDP listen on all network interfaces:

chrome.exe --remote-debugging-port=9222 --remote-debugging-address=0.0.0.0

Or configure port forwarding using Windows netsh:

netsh interface portproxy add v4tov4 listenport=9222 listenaddress=0.0.0.0 connectport=9222 connectaddress=127.0.0.1

Important

--remote-debugging-address=0.0.0.0 exposes the CDP port to the local network, which is a security risk. Only use this in internal development environments. For production, always combine with firewall rules to restrict access.

Scenario 3: Chrome Ignores --remote-debugging-portโ€‹

Problemโ€‹

Chrome is already running. Launching Chrome again with --remote-debugging-port=9222 silently ignores the flag. Chrome simply opens a new tab in the existing window, and the CDP port is not opened:

# Chrome already running
chrome.exe --remote-debugging-port=9222
# No error, but port 9222 is not listening

Root Causeโ€‹

Chrome is designed as a single-instance application. When it detects an existing Chrome process, the newly launched Chrome forwards the URL (and other command-line arguments) to the existing process and exits. Flags like --remote-debugging-port only take effect when the process is first created โ€” the existing process never dynamically loads these parameters.

Solutionโ€‹

Close all Chrome processes first, then relaunch with the flag:

# Windows
taskkill /F /IM chrome.exe
chrome.exe --remote-debugging-port=9222

# macOS
pkill -f "Google Chrome"
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222

You can also use --user-data-dir to specify a separate profile directory, avoiding conflicts with your daily Chrome:

chrome.exe --remote-debugging-port=9222 --user-data-dir="C:\chrome-debug-profile"

The three scenarios above show that relying on an external Chrome process for cookie extraction is too fragile under WSL2 + anti-bot detection + process management constraints. The final solution: replace external Chrome with Electron's BrowserWindow.

Why Electron Worksโ€‹

  1. No anti-bot detection: Electron's built-in Chromium shares the same rendering engine as Chrome. Anti-bot systems do not flag it as an automated environment.
  2. No external Chrome dependency: No need to manage Chrome processes, CDP ports, or profile directories.
  3. No WSL2 networking issues: Electron runs natively on the target OS โ€” no cross-OS network isolation problems.
  4. No single-instance conflicts: Electron creates its own BrowserWindow โ€” no conflict with the user's daily Chrome.

Complete Implementationโ€‹

Open login window and poll for cookies:

import { BrowserWindow } from 'electron';

let cookieWindow = null;

function openLoginWindow() {
cookieWindow = new BrowserWindow({
width: 1000,
height: 700,
title: 'Login to Target Platform',
webPreferences: {
// Key: use isolated session, separate from main window
partition: 'cookie-login',
contextIsolation: true,
nodeIntegration: false,
},
});

cookieWindow.loadURL('https://target-site.com/login');

// Poll for target cookie
const interval = setInterval(async () => {
const cookies = await cookieWindow.webContents.session.cookies.get({
domain: '.target-site.com',
});

const sessionCookie = cookies.find((c) => c.name === 'session_token');
if (sessionCookie) {
clearInterval(interval);

// Build full cookie string
const cookieStr = cookies
.map((c) => c.name + '=' + c.value)
.join('; ');

// Write to environment variable
process.env.SESSION_COOKIE = cookieStr;
updateEnvFile('SESSION_COOKIE', cookieStr);

cookieWindow.close();
}
}, 2000);

cookieWindow.on('closed', () => {
clearInterval(interval);
cookieWindow = null;
});
}

Preload script exposing IPC interface:

import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('electronAPI', {
extractCookie: () => ipcRenderer.invoke('extract-cookie'),
onCookieExtracted: (callback) =>
ipcRenderer.on('cookie-extracted', (_, data) => callback(data)),
});

Renderer process usage:

// Detect Electron environment
if (window.electronAPI) {
document.getElementById('extractBtn').addEventListener('click', () => {
window.electronAPI.extractCookie();
});

window.electronAPI.onCookieExtracted((data) => {
console.log('Cookie extracted:', data);
});
}

Important

  • partition: 'cookie-login' creates an isolated session. The login window's cookies won't pollute the main window. If you need to share login state, remove the partition option or use the same partition name.
  • A 2-second polling interval balances UX and performance. Don't replace setInterval with a while + await loop โ€” that would block the renderer process.
  • session.cookies.get() only returns cookies from the current session. You cannot read cookies across partitions.

FAQโ€‹

What to do when Puppeteer stealth plugin is still detected by anti-bot?โ€‹

The stealth plugin only bypasses basic fingerprint checks (like navigator.webdriver). Advanced anti-bot systems detect automation through browser behavior and low-level API characteristics, including Chromium build signatures, Canvas rendering differences, and mouse trajectory patterns. The stealth plugin cannot fully cover all detection vectors. Use Electron BrowserWindow to load the target site and extract cookies via session.cookies.get() after the user logs in manually.

Why is Chrome --remote-debugging-port not working?โ€‹

Chrome uses a single-process architecture. When a Chrome process is already running, the --remote-debugging-port flag is silently ignored. The newly launched Chrome simply opens a new tab in the existing instance without enabling the debugging port. You need to close all Chrome processes first using taskkill /F /IM chrome.exe (Windows) or pkill -f "Google Chrome" (macOS), then relaunch with the flag. Alternatively, use --user-data-dir to specify a separate profile directory to avoid conflicts.

How to connect to Chrome debugging port from WSL2?โ€‹

Chrome's --remote-debugging-port binds to Windows 127.0.0.1 by default, but WSL2 has its own network stack where localhost points to the Linux loopback address. Two solutions: first, add --remote-debugging-address=0.0.0.0 when launching Chrome to make CDP listen on all interfaces; second, configure port forwarding on Windows using netsh interface portproxy to forward WSL2 requests to the Windows CDP port.


Need help with data collection or automation tools?

Contact Us

Container Port Unreachable from WSL2? The Docker Desktop network_mode:host Trap

ยท 3 min read

Encountered this issue while building an AI data analytics platform (Airflow + PostgreSQL) for a client. Here's the root cause and solution.

TL;DRโ€‹

On Docker Desktop for Windows (WSL2 backend), network_mode: host container ports are unreachable from the WSL2 host. The container shows the port listening, but curl localhost:PORT returns connection refused. The fix: use network_mode: !reset in your override file to remove host mode, then switch to bridge + external network + port mapping.

Fix WSL2 Cannot Access Windows Host Proxy โ€” Three Invisible Pitfalls

ยท 5 min read

Encountered this issue while using AI coding tools (Claude Code, Roo) in WSL2 that need to access APIs through the Windows host proxy. After fixing the firewall, encountered two more hidden pitfalls. Documenting all root causes and solutions.

TL;DRโ€‹

WSL2's vEthernet (WSL) virtual NIC is created on every launch. Windows Firewall cannot assign a Network Profile to it, so all inbound rules are ineffective (EnforcementStatus: NotApplicable). Don't add rules โ€” disable the firewall on that interface directly:

# Run in Windows PowerShell (Administrator)
Set-NetFirewallProfile -DisabledInterfaceAliases "vEthernet (WSL)"

Additionally, after fixing the firewall, you may encounter two more pitfalls:

  1. Dynamic IP Issue: Host IP changes after WSL/Windows restart
  2. Config Cache Issue: Stale API keys in ~/.claude.json and ~/.claude/settings.json cause auth conflicts