Repository: urllib3/urllib3

Issue:
Add support for HTTP/2 chunked request bodies
HTTP/2 doesn't support the `Transfer-Encoding: chunked` header because it doesn't need the header or additional framing, instead relying on the stream framing mechanism. If users send a request with `chunked=True` we should not emit the above mentioned header and send the request body without the additional framing.


Discussion:
@sethmlarson Looking at the code, I think that we already [ignore](https://github.com/urllib3/urllib3/blob/main/src/urllib3/http2.py#L267-L270) the `chunked` kwarg and do not emit the `Transfer-Encoding` header. This implies that there is no need for this task?

Looking at the discussion [here](https://github.com/urllib3/urllib3/pull/3421#discussion_r1666195312), maybe the goal is to raise an exception if chunked encoding is requested? In that case, could you have a look at [PR-3429](https://github.com/urllib3/urllib3/pull/3429). It's in draft right now since I am not sure if it's indeed what's needed here.



Patch:
diff --git a/changelog/3425.bugfix.rst b/changelog/3425.bugfix.rst
new file mode 100644
index 0000000000..148423db1c
--- /dev/null
+++ b/changelog/3425.bugfix.rst
@@ -0,0 +1,1 @@
+Excluded Transfer-Encoding: chunked from HTTP/2 request body
diff --git a/src/urllib3/http2/connection.py b/src/urllib3/http2/connection.py
index d9f06ca36a..358c9fc4b0 100644
--- a/src/urllib3/http2/connection.py
+++ b/src/urllib3/http2/connection.py
@@ -276,7 +276,10 @@ def request(  # type: ignore[override]
 
         headers = headers or {}
         for k, v in headers.items():
-            self.putheader(k, v)
+            if k.lower() == "transfer-encoding" and v == "chunked":
+                continue
+            else:
+                self.putheader(k, v)
 
         if b"user-agent" not in dict(self._headers):
             self.putheader(b"user-agent", _get_default_user_agent())


Test patch:
diff --git a/test/test_http2_connection.py b/test/test_http2_connection.py
index 9a8b59deaf..52c754ee83 100644
--- a/test/test_http2_connection.py
+++ b/test/test_http2_connection.py
@@ -325,3 +325,36 @@ def test_close(self) -> None:
         sendall.assert_called_with(b"foo")
         assert conn._h2_stream is None
         assert conn._headers == []
+
+    def test_request_ignore_chunked(self) -> None:
+        conn = HTTP2Connection("example.com")
+        conn.sock = mock.MagicMock(
+            sendall=mock.Mock(return_value=None),
+        )
+        sendall = conn.sock.sendall
+        data_to_send = conn._h2_conn._obj.data_to_send = mock.Mock(return_value=b"foo")
+        send_headers = conn._h2_conn._obj.send_headers = mock.Mock(return_value=None)
+        conn._h2_conn._obj.send_data = mock.Mock(return_value=None)
+        conn._h2_conn._obj.get_next_available_stream_id = mock.Mock(return_value=1)
+        close_connection = conn._h2_conn._obj.close_connection = mock.Mock(
+            return_value=None
+        )
+
+        conn.request("GET", "/", headers={"Transfer-Encoding": "chunked"}, chunked=True)
+        conn.close()
+
+        data_to_send.assert_called_with()
+        sendall.assert_called_with(b"foo")
+        send_headers.assert_called_with(
+            stream_id=1,
+            headers=[
+                (b":scheme", b"https"),
+                (b":method", b"GET"),
+                (b":authority", b"example.com:443"),
+                (b":path", b"/"),
+                (b"user-agent", _get_default_user_agent().encode()),
+            ],
+            end_stream=True,
+        )
+
+        close_connection.assert_called_with()