Repository: urllib3/urllib3

Issue:
PoolManager does not support argument blocksize
### Subject

urllib3's PoolManager does not support `blocksize`. This seems to be a new argument accepted by Python's standard library's http clients since 3.7 (https://docs.python.org/3.6/library/http.client.html vs https://docs.python.org/3.7/library/http.client.html)

Adding `key_blocksize` to poolmanager.py's `_key_fields` fixes the issue but it probably should only be appended when it is actually supported.

### Environment

OS Linux-5.4.0-47-generic-x86_64-with-glibc2.29
Python 3.8.5
urllib3 1.26.2

### Steps to Reproduce

```
from urllib3 import PoolManager
with PoolManager(blocksize = 8192) as pool:
    response = pool.urlopen("GET", "https://www.google.com")
```

### Expected Behavior

Should perform a GET request successfully.

### Actual Behavior

The following exception is thrown

```
Traceback (most recent call last):
  File "report.py", line 13, in <module>
    response = pool.urlopen("GET", "https://www.google.com")
  File "/home/geancarlo/redacted/poc/venv/lib/python3.8/site-packages/urllib3/poolmanager.py", line 364, in urlopen
    conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme)
  File "/home/geancarlo/redacted/poc/venv/lib/python3.8/site-packages/urllib3/poolmanager.py", line 245, in connection_from_host
    return self.connection_from_context(request_context)
  File "/home/geancarlo/redacted/poc/venv/lib/python3.8/site-packages/urllib3/poolmanager.py", line 258, in connection_from_context
    pool_key = pool_key_constructor(request_context)
  File "/home/geancarlo/redacted/poc/venv/lib/python3.8/site-packages/urllib3/poolmanager.py", line 124, in _default_key_normalizer
    return key_class(**context)
TypeError: __new__() got an unexpected keyword argument 'key_blocksize'
```



Discussion:
I've been thinking about `blocksize` recently actually so thanks for opening this.

We should be defaulting `blocksize` to `16384` I believe since this is what we're using as a read/write size for pyOpenSSL and SecureTransport, but we should also support setting it.
This is probably doable by:

 * appending `key_blocksize` to `_key_fields` if `sys.version_info >= (3, 7)`
 * copying `test_http_pool_key_fields` but call it `test_http_pool_key_blocksize`, decorate it with `@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python 3.7+")` and test `key_blocksize` specifically

@geancarlo Would you like to try adding this feature?
Hi. Yes, I can do it. This weekend I'll probably have some time
@geancarlo are you working on this? I would be happy to take over if you are not?
I will work on this, will refer @geancarlo PR. 

Resolving patch:
diff --git a/src/urllib3/poolmanager.py b/src/urllib3/poolmanager.py
--- a/src/urllib3/poolmanager.py
+++ b/src/urllib3/poolmanager.py
@@ -56,6 +56,9 @@
     "ssl_context",
     "key_password",
 )
+# Default value for `blocksize` - a new parameter introduced to
+# http.client.HTTPConnection & http.client.HTTPSConnection in Python 3.7
+_DEFAULT_BLOCKSIZE = 16384
 
 _SelfT = TypeVar("_SelfT")
 
@@ -95,6 +98,7 @@ class PoolKey(NamedTuple):
     key_assert_hostname: Optional[Union[bool, str]]
     key_assert_fingerprint: Optional[str]
     key_server_hostname: Optional[str]
+    key_blocksize: Optional[int]
 
 
 def _default_key_normalizer(
@@ -145,6 +149,10 @@ def _default_key_normalizer(
         if field not in context:
             context[field] = None
 
+    # Default key_blocksize to _DEFAULT_BLOCKSIZE if missing from the context
+    if context.get("key_blocksize") is None:
+        context["key_blocksize"] = _DEFAULT_BLOCKSIZE
+
     return key_class(**context)
 
 
@@ -250,6 +258,11 @@ def _new_pool(
         if request_context is None:
             request_context = self.connection_pool_kw.copy()
 
+        # Default blocksize to _DEFAULT_BLOCKSIZE if missing or explicitly
+        # set to 'None' in the request_context.
+        if request_context.get("blocksize") is None:
+            request_context["blocksize"] = _DEFAULT_BLOCKSIZE
+
         # Although the context has everything necessary to create the pool,
         # this function has historically only used the scheme, host, and port
         # in the positional args. When an API change is acceptable these can


Test patch:
diff --git a/test/test_poolmanager.py b/test/test_poolmanager.py
--- a/test/test_poolmanager.py
+++ b/test/test_poolmanager.py
@@ -9,7 +9,12 @@
 from urllib3 import connection_from_url
 from urllib3.connectionpool import HTTPSConnectionPool
 from urllib3.exceptions import ClosedPoolError, LocationValueError
-from urllib3.poolmanager import PoolKey, PoolManager, key_fn_by_scheme
+from urllib3.poolmanager import (
+    _DEFAULT_BLOCKSIZE,
+    PoolKey,
+    PoolManager,
+    key_fn_by_scheme,
+)
 from urllib3.util import retry, timeout
 from urllib3.util.url import Url
 
@@ -113,6 +118,7 @@ def test_http_pool_key_fields(self) -> None:
             "retries": retry.Retry(total=6, connect=2),
             "block": True,
             "source_address": "127.0.0.1",
+            "blocksize": _DEFAULT_BLOCKSIZE + 1,
         }
         p = PoolManager()
         conn_pools = [
@@ -145,6 +151,7 @@ def test_https_pool_key_fields(self) -> None:
             "cert_reqs": "CERT_REQUIRED",
             "ca_certs": "/root/path_to_pem",
             "ssl_version": "SSLv23_METHOD",
+            "blocksize": _DEFAULT_BLOCKSIZE + 1,
         }
         p = PoolManager()
         conn_pools = [
@@ -401,3 +408,23 @@ def test_pool_manager_no_url_absolute_form(self) -> None:
         p = PoolManager()
         assert p._proxy_requires_url_absolute_form(Url("http://example.com")) is False
         assert p._proxy_requires_url_absolute_form(Url("https://example.com")) is False
+
+    @pytest.mark.parametrize(
+        "input_blocksize,expected_blocksize",
+        [
+            (_DEFAULT_BLOCKSIZE, _DEFAULT_BLOCKSIZE),
+            (None, _DEFAULT_BLOCKSIZE),
+            (8192, 8192),
+        ],
+    )
+    def test_poolmanager_blocksize(
+        self, input_blocksize: int, expected_blocksize: int
+    ) -> None:
+        """Assert PoolManager sets blocksize properly"""
+        p = PoolManager()
+
+        pool_blocksize = p.connection_from_url(
+            "http://example.com", {"blocksize": input_blocksize}
+        )
+        assert pool_blocksize.conn_kw["blocksize"] == expected_blocksize
+        assert pool_blocksize._get_conn().blocksize == expected_blocksize  # type: ignore[attr-defined]