diff --git a/lib/_http_client.js b/lib/_http_client.js index 68eb125e0e10e9..2e6795ddf93270 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -252,6 +252,11 @@ function createHangUpError() { return error; } +function createConnectError(statusCode) { + var error = new Error('connect error: ' + statusCode); + error.code = 'ECONNREFUSED'; + return error; +} function socketCloseListener() { var socket = this; @@ -431,6 +436,29 @@ function parserOnIncomingClient(res, shouldKeepAlive) { // Responses to CONNECT request is handled as Upgrade. if (req.method === 'CONNECT') { + if (res.statusCode >= 400) { + // this happens when proxy refuses to tunnel and returns a 403. + // detach the socket and remove the listeners that we installed + socket.emit('agentRemove'); + socket.removeListener('close', socketCloseListener); + socket.removeListener('error', socketErrorListener); + socket.removeListener('data', socketOnData); + socket.removeListener('end', socketOnEnd); + + // Emit the error + req.emit('error', createConnectError(res.statusCode)); + // http_parser will still emit a Parse Error because it is not + // expecting a body. + // Make sure that further errors will be silently ignored. + req.removeAllListeners('error'); + req.on('error', function() {}); + // emit the close event and destroy the socket. + req.emit('close'); + socket.destroy(); + + // tell parser to skip the body + return true; + } res.upgrade = true; return 2; // skip body, and the rest } diff --git a/test/parallel/test-http-connect-error.js b/test/parallel/test-http-connect-error.js new file mode 100644 index 00000000000000..0d5dddb624ad35 --- /dev/null +++ b/test/parallel/test-http-connect-error.js @@ -0,0 +1,92 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); +var http = require('http'); + +var serverGotConnect = false; +var clientGotError = false; +var clientGotClose = false; + +var server = http.createServer(function(req, res) { + assert(false); +}); +server.on('connect', function(req, socket, firstBodyChunk) { + assert.equal(req.method, 'CONNECT'); + assert.equal(req.url, 'google.com:444'); + console.error('Server got CONNECT request'); + serverGotConnect = true; + + socket.write('HTTP/1.1 403 Forbidden\r\nContent-Type: plain/text' + + '\r\nContent-Length: 7\r\n\r\nrefused'); + + var data = firstBodyChunk.toString(); + socket.on('data', function(buf) { + data += buf.toString(); + }); + socket.on('end', function() { + socket.end(data); + }); +}); +server.listen(common.PORT, function() { + var req = http.request({ + port: common.PORT, + method: 'CONNECT', + path: 'google.com:444' + }, function(res) { + assert(false); + }); + + req.on('close', function() { + console.error('Client got close'); + clientGotClose = true; + + // Make sure this request got removed from the pool. + var name = 'localhost:' + common.PORT; + assert(!http.globalAgent.sockets.hasOwnProperty(name)); + assert(!http.globalAgent.requests.hasOwnProperty(name)); + + // Make sure this socket has detached. + var socket = req.socket; + assert(!socket.ondata); + assert(!socket.onend); + + assert.equal(socket.listeners('connect').length, 0); + assert.equal(socket.listeners('data').length, 0); + + // the stream.Duplex onend listener + // allow 0 here, so that i can run the same test on streams1 impl + assert(socket.listeners('end').length <= 1); + + assert.equal(socket.listeners('free').length, 0); + assert.equal(socket.listeners('close').length, 0); + assert.equal(socket.listeners('error').length, 0); + assert.equal(socket.listeners('agentRemove').length, 0); + + // Close server to terminate test + server.close(); + + }); + + req.on('connect', function(res, socket, firstBodyChunk) { + assert(false); + }); + + req.on('error', function(err) { + console.error('Client got error'); + clientGotError = true; + assert(err.message === 'connect error: 403'); + assert(err.code === 'ECONNREFUSED'); + }); + + // It is legal for the client to send some data intended for the server + // before the "200 Connection established" (or any other success or + // error code) is received. + req.write('Head'); + req.end(); +}); + +process.on('exit', function() { + assert.ok(serverGotConnect); + assert.ok(clientGotError); + assert.ok(clientGotClose); +});