Repository: urllib3/urllib3

Issue:
File descriptor leak with HTTPS proxies via CONNECT
It looks like when making HTTPS requests through an HTTP proxy, there is a file descriptor leak if an error occurs (e.g. socket timeout or 404 error) during httplib.HTTPConnection._tunnel. Tested in versions 1.9, 1.9.1, 1.10.

The way I've been testing is to use netcat to listen for connections but not respond to CONNECT, eg.:

```
$ nc -l localhost 8000
```

Then making a request with that "proxy":

```
In [1]: import urllib3
In [2]: pm = urllib3.ProxyManager('http://localhost:8000/')
In [3]: r = pm.request('GET', 'https://httpbin.org/ip', timeout=1.0)
---------------------------------------------------------------------------
MaxRetryError                             Traceback (most recent call last)
```

At that point, `lsof` shows multiple connections (one per retry) in CLOSE_WAIT state, which stay that way permanently.

It looks like the issue is related to a circular reference preventing a socket from being cleaned up properly. For example, this commit fixes the issue: https://github.com/ianpreston/urllib3/commit/a0953a93c877468d9b60a6785c767c5ac31d0dc2 , as does wrapping the `raise` statements in `Retry.increment` with a "finally: del _stacktrace".

The workaround above is working for now but I'd like to find the root cause, or at least a workaround that doesn't break stack traces. Any idea where to start looking? Thanks



Discussion:
@ianpreston Great find, thank you!

Honestly I'm not very experienced with finding memory leaks in Python. @sigmavirus24 @Lukasa?

Wrapping with `finally: del _stacktrace` sounds reasonable. Another approach is we could use weakrefs where appropriate. Would you like to send a PR to this effect? :)

I'm going to admit to a degree of confusion here.

How can the stacktrace object be keeping the socket alive? That stacktrace object passed to the retry increment method doesn't get saved on the object, it only gets thrown. Unless `six.reraise` is holding on to it somewhere, I don't see the circular reference. Can you enlighten me?

@Lukasa My understanding is that each frame of the traceback object returned by `sys.exc_info` holds references to the locals of that stack frame. So if it gets assigned to a local variable, there will be a `locals -> frame -> locals` loop.

My thinking is that there's a socket assigned to a local variable somewhere (possibly httplib) that's being opened but not properly closed. Since CPython closes file descriptors when their refcount hits zero, removing the cycle causes the sockets to be closed. This is all speculation, though. I could be totally wrong on that.

I'll have some time later this week to dig a little deeper. Will at least submit a PR with `finally: del _stacktrace` wrappers.

Can someone see if `objgraph` agrees with @ianpreston's understanding above?


Resolving patch:
diff --git a/urllib3/connectionpool.py b/urllib3/connectionpool.py
--- a/urllib3/connectionpool.py
+++ b/urllib3/connectionpool.py
@@ -588,14 +588,13 @@ def urlopen(self, method, url, body=None, headers=None, retries=None,
                 conn.close()
                 conn = None
 
-            stacktrace = sys.exc_info()[2]
             if isinstance(e, SocketError) and self.proxy:
                 e = ProxyError('Cannot connect to proxy.', e)
             elif isinstance(e, (SocketError, HTTPException)):
                 e = ProtocolError('Connection aborted.', e)
 
-            retries = retries.increment(method, url, error=e,
-                                        _pool=self, _stacktrace=stacktrace)
+            retries = retries.increment(method, url, error=e, _pool=self,
+                                        _stacktrace=sys.exc_info()[2])
             retries.sleep()
 
             # Keep track of the error for the retry warning.
diff --git a/urllib3/util/connection.py b/urllib3/util/connection.py
--- a/urllib3/util/connection.py
+++ b/urllib3/util/connection.py
@@ -82,6 +82,7 @@ def create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
             err = _
             if sock is not None:
                 sock.close()
+                sock = None
 
     if err is not None:
         raise err


Test patch: