fix(agent-base): use duck typing for agent detection in createSocket#399
Conversation
Use duck typing (checking for addRequest method) instead of strict instanceof http.Agent check, consistent with Node.js core behavior. Node.js _http_client.js uses duck typing for options.agent - it only checks typeof agent.addRequest === 'function'. However, agent-base's createSocket() uses instanceof http.Agent, which fails for agents that implement addRequest() but inherit from EventEmitter instead of http.Agent (e.g. tunnel-agent's TunnelingAgent). When connect() returns such a duck-typed agent, createSocket() treats it as a raw socket, passing it to Node's HTTP machinery which expects write()/read() methods. The request then hangs indefinitely. This was discovered while debugging the vs-picgo VSCode extension, which uses tunnel-agent for HTTPS-over-HTTP proxy tunneling. When VSCode's @vscode/proxy-agent intercepts the request (for system certificate injection) and the proxy resolver returns DIRECT, the TunnelingAgent is passed back through agent-base's createSocket(). The instanceof check fails, the agent is treated as a raw socket, and the image upload hangs for 30 seconds until PicGo's timeout fires.
🦋 Changeset detectedLatest commit: aa65c52 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
@fjh1997 is attempting to deploy a commit to the Nathan Rajlich's projects Team on Vercel. A member of the Team first needs to authorize it. |
TooTallNate
left a comment
There was a problem hiding this comment.
Seems fine to me, we can probably drop the instanceof check entirely then. Can you add a unit test?
There was a problem hiding this comment.
Done in d82810f:
- Dropped the
instanceof http.Agentcheck entirely — now purely duck-typed viaaddRequest - Added a unit test that returns a plain object with
addRequest(not anhttp.Agentinstance) to verify the duck typing works - Fixed the resulting TypeScript narrowing issues with explicit casts
|
The bot nailed it — clean refactor, proper test coverage, even handled the TypeScript narrowing correctly. All 16 tests pass. Curious, what model is powering clutz-bot? Claude, Codex, or something else? |
|
@clutz-bot lol, i didn't expect you to push a fix to someone else's PR. But while we're at it, how about you fix the lint failure? @fjh1997 It's Opus 4.6. |
|
Fixed the lint warnings in |
|
Haha no way, I'm literally using Opus 4.6 right now to debug this whole issue! The entire root cause analysis — instrumenting @vscode/proxy-agent, tracing through agent-base's createSocket, figuring out the _deferToConnect hang mechanism — was all done with Opus 4.6 in Claude Code. Small world. Though my wallet is definitely feeling it lol, that model is not cheap. |
Summary
agent-base'screateSocket()usessocket instanceof http.Agentto determine whether the return value ofconnect()is an agent (calladdRequest()) or a raw socket. This is stricter than Node.js core, which uses duck typing (typeof agent.addRequest === 'function'in_http_client.js).This causes agents that implement
addRequest()but don't inherit fromhttp.Agentto be misidentified as raw sockets, causing HTTP requests to hang indefinitely without any error.The fix adds a duck typing fallback:
Background: how this bug was discovered
While using the vs-picgo VSCode extension to upload images to GitHub, every upload would hang for exactly 30 seconds before succeeding on retry.
Root cause chain:
requestlibrary withtunnel-agentfor HTTPS-over-HTTP proxy tunnelingtunnel-agent'sTunnelingAgentinherits fromEventEmitter, nothttp.Agent— this is valid since Node.js core only requires duck typing foroptions.agent@vscode/proxy-agentpatcheshttps.request()and intercepts the request (for system certificate injection,addCertificatesV1=true)DIRECT, soPacProxyAgent.connect()returns the originalTunnelingAgentas-isagent-base'screateSocket()checksTunnelingAgent instanceof http.Agent→ falseTunnelingAgentis stored ascurrentSocket, then returned viacreateConnection()to Node's HTTP client as if it were a raw TCP socketExact hang mechanism (from Node.js
_http_outgoing.js_deferToConnect):Since
TunnelingAgent.writableisundefined(falsy), Node thinks the "socket" isn't connected yet and waits for a'connect'event. ButTunnelingAgentis anEventEmitterthat never emits'connect', so the request hangs indefinitely — no error, no timeout, no output. PicGo's 30-secondREQUEST_TIMEOUT_MSeventually fires, and the retry withproxy: nulluseshttps.globalAgent(a realhttp.Agent), which succeeds.Reproduction:
Why this should be fixed in agent-base
_http_client.js) uses duck typing foroptions.agent— only checkstypeof agent.addRequest === 'function'tunnel-agent(billions of downloads, used byrequestlibrary) relies on this duck typing contractagent-baseis the layer that introduces the stricterinstanceofcheck, creating an incompatibility with legitimate duck-typed agentsagent-basesubclass (e.g.PacProxyAgent,HttpsProxyAgent,SocksProxyAgent) that returns a duck-typed agent fromconnect()will trigger this bugTrigger conditions
This bug is triggered when all of the following conditions are met:
agent-basesubclass'sconnect()method returns an object that:addRequest()method (duck-typed agent)http.Agent(failsinstanceofcheck)tunnel-agent(e.g.,requestwith proxy config)http.proxySupportis set toon(default)@vscode/proxy-agentintercepts the request for certificate injectionDIRECT, passing the originalTunnelingAgentthroughTest plan
tunnel-agent(e.g. vs-picgo) no longer hang when proxy resolver returns DIRECThttp.Agentinstances still work correctlyaddRequest) still work correctly