diff --git a/PRE_RELEASE_CHANGELOG.md b/PRE_RELEASE_CHANGELOG.md index dd82c06d..20034926 100644 --- a/PRE_RELEASE_CHANGELOG.md +++ b/PRE_RELEASE_CHANGELOG.md @@ -1 +1,7 @@ # Pre-Release Changelog + +## Unreleased + +## Bug Fixes + +- **Proxy Agent Streams:** Guarded Node HTTP adapter TCP keep-alive setup so proxy agents that return generic Duplex streams do not throw when `setKeepAlive` is unavailable. (**#10917**, closes **#10908**) \ No newline at end of file diff --git a/lib/adapters/http.js b/lib/adapters/http.js index b95795b3..c9a6b609 100755 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -1205,7 +1205,11 @@ export default isHttpAdapterSupported && req.on('socket', function handleRequestSocket(socket) { // default interval of sending ack packet is 1 minute - socket.setKeepAlive(true, 1000 * 60); + // proxy agents (e.g. agent-base) may return a generic Duplex stream + // that doesn't have setKeepAlive, so guard before calling + if (typeof socket.setKeepAlive === 'function') { + socket.setKeepAlive(true, 1000 * 60); + } // Install a single 'error' listener per socket (not per request) to avoid // accumulating listeners on pooled keep-alive sockets that get reassigned diff --git a/tests/unit/adapters/http.test.js b/tests/unit/adapters/http.test.js index 6aee7d16..959ace72 100644 --- a/tests/unit/adapters/http.test.js +++ b/tests/unit/adapters/http.test.js @@ -6169,6 +6169,58 @@ describe('supports http with nodejs', () => { 'second request should be destroyed by its own active socket error' ); }); + + it('should not throw TypeError when a proxy agent stream does not define setKeepAlive (regression #10908)', async () => { + // proxy agents (e.g. agent-base) may provide a generic Duplex stream as + // the socket; that stream does not define setKeepAlive. + const socket = new stream.Duplex({ + read() {}, + write(_chunk, _encoding, callback) { + callback(); + }, + }); + assert.strictEqual(typeof socket.setKeepAlive, 'undefined'); + + const transport = { + request(_, cb) { + return new (class MockRequest extends EventEmitter { + constructor() { + super(); + this.destroyed = false; + } + + setTimeout() {} + write() {} + + end() { + this.emit('socket', socket); + + setImmediate(() => { + const response = stream.Readable.from(['ok']); + response.statusCode = 200; + response.headers = {}; + cb(response); + this.emit('close'); + }); + } + + destroy(err) { + if (this.destroyed) return; + this.destroyed = true; + err && this.emit('error', err); + this.emit('close'); + } + })(); + }, + }; + + const result = await axios.get('http://example.com/', { + transport, + maxRedirects: 0, + }); + + assert.strictEqual(result.status, 200); + }); }); describe('redirect listener accumulation', () => {