Repository: urllib3/urllib3

Issue:
"AttributeError: 'NoneType' object has no attribute 'strip'" with malformed URL
```
>>> import urllib3
>>> http = urllib3.PoolManager()
>>> r = http.request('GET', 'http://@')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../urllib3/request.py", line 75, in request
    **urlopen_kw)
  File ".../urllib3/request.py", line 88, in request_encode_url
    return self.urlopen(method, url, **urlopen_kw)
  File ".../urllib3/poolmanager.py", line 145, in urlopen
    conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme)
  File ".../urllib3/poolmanager.py", line 119, in connection_from_host
    pool = self._new_pool(scheme, host, port)
  File ".../urllib3/poolmanager.py", line 86, in _new_pool
    return pool_cls(host, port, **kwargs)
  File ".../urllib3/connectionpool.py", line 226, in __init__
    ConnectionPool.__init__(self, host, port)
  File ".../urllib3/connectionpool.py", line 156, in __init__
    host = host.strip('[]')
AttributeError: 'NoneType' object has no attribute 'strip'
```

The fundamental issue here seems to be that `connection_from_host` assumes `host` will never be `None`, but

```
>>> urllib3.util.parse_url('http://@')
Url(scheme='http', auth='', host=None, port=None, path=None, query=None, fragment=None)
```

I suggest that `LocationParseError` should be thrown instead under these conditions, and that perhaps `parse_url` should do it (since it does not appear to support URL-schemes that don't have a netloc, and netloc-ful schemes with an empty host don't make sense).

For the record, this is _not_ academic - I actually encountered a webserver that returned a redirection to "`http://@`" while doing a moderate-scale survey of the interwebs.



Discussion:
Sounds reasonable to me, what do you think @shazow?

On thinking about it a little more, maybe it's better to leave `parse_url` alone - it's documented to be usable for relative URLs after all.  In that case I dunno whether it makes more sense to throw `LocationParseError` from `PoolManager.connection_from_url` or `ConnectionPool.__init__`.

Hmm. IMO `parse_url` should parse an empty-host string if possible as `host=None`. At that point, the Connection-related class should raise the appropriate exception.

What do you guys think? (Anyone interested in doing this?)


Resolving patch:
diff --git a/urllib3/connectionpool.py b/urllib3/connectionpool.py
--- a/urllib3/connectionpool.py
+++ b/urllib3/connectionpool.py
@@ -23,6 +23,7 @@
     ConnectTimeoutError,
     EmptyPoolError,
     HostChangedError,
+    LocationParseError,
     MaxRetryError,
     SSLError,
     TimeoutError,
@@ -40,7 +41,6 @@
 from .request import RequestMethods
 from .response import HTTPResponse
 from .util import (
-    assert_fingerprint,
     get_host,
     is_connection_dropped,
     Timeout,
@@ -65,6 +65,9 @@ class ConnectionPool(object):
     QueueCls = LifoQueue
 
     def __init__(self, host, port=None):
+        if host is None:
+            raise LocationParseError(host)
+
         # httplib doesn't like it when we include brackets in ipv6 addresses
         host = host.strip('[]')
 
diff --git a/urllib3/util/url.py b/urllib3/util/url.py
--- a/urllib3/util/url.py
+++ b/urllib3/util/url.py
@@ -131,7 +131,7 @@ def parse_url(url):
         if port:
             # If given, ports must be integers.
             if not port.isdigit():
-                raise LocationParseError("Failed to parse: %s" % url)
+                raise LocationParseError(url)
             port = int(port)
         else:
             # Blank ports are cool, too. (rfc3986#section-3.2.3)


Test patch:
diff --git a/test/test_poolmanager.py b/test/test_poolmanager.py
--- a/test/test_poolmanager.py
+++ b/test/test_poolmanager.py
@@ -2,7 +2,10 @@
 
 from urllib3.poolmanager import PoolManager
 from urllib3 import connection_from_url
-from urllib3.exceptions import ClosedPoolError
+from urllib3.exceptions import (
+    ClosedPoolError,
+    LocationParseError,
+)
 
 
 class TestPoolManager(unittest.TestCase):
@@ -63,6 +66,9 @@ def test_manager_clear(self):
         self.assertEqual(len(p.pools), 0)
 
 
+    def test_nohost(self):
+        p = PoolManager(5)
+        self.assertRaises(LocationParseError, p.connection_from_url, 'http://@')
 
 
 if __name__ == '__main__':
diff --git a/test/test_util.py b/test/test_util.py
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -77,6 +77,7 @@ def test_invalid_host(self):
         for location in invalid_host:
             self.assertRaises(LocationParseError, get_host, location)
 
+
     def test_parse_url(self):
         url_host_map = {
             'http://google.com/mail': Url('http', host='google.com', path='/mail'),
@@ -107,6 +108,7 @@ def test_parse_url(self):
             'http://foo:bar@localhost/': Url('http', auth='foo:bar', host='localhost', path='/'),
             'http://foo@localhost/': Url('http', auth='foo', host='localhost', path='/'),
             'http://foo:bar@baz@localhost/': Url('http', auth='foo:bar@baz', host='localhost', path='/'),
+            'http://@': Url('http', host=None, auth='')
         }
         for url, expected_url in url_host_map.items():
             returned_url = parse_url(url)