Repository: urllib3/urllib3

Issue:
Add http_version property to BaseHTTPResponse
Now that HTTP/2 is coming we should add a value to HTTPResponse to provide this value. The normal HTTPResponse will either be HTTP/1.1 or HTTP/1.0. Check `_http_vsn_str` for that value.


Discussion:
Turns out `BaseHTTPResponse` already has a `version` field, which gets populated from `http.client.HTTPVersion.version`. It's an integer mapped from the [HTTP-version field](https://www.rfc-editor.org/rfc/rfc9112.html#name-http-version) in the start line. For example, `HTTP/1.1` is mapped to 11: https://github.com/python/cpython/blob/b822b85ac11e73bbe4417bf03ee770ab116bb42d/Lib/http/client.py#L342-L348.

While arguably more ugly than using the full string or even the [ALPN name](https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids), according to [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-protocol-version) it's OK to assume that all HTTP protocols will be two digits (major and minor), with the second digit being 0 if not specified.

Two decisions are needed here:

 1. Do we add a new `http_version` field and fill it with `HTTP/1.1` or `HTTP/2`?
 2. Do we keep the current `version` field and use 20 for HTTP/2?

My loosely held opinion is 1. No 2. Yes.
So I'm new to the codebase but I've been doing a bit of research into this past few hours, came to report essentially what @pquentin has already brought up!

It does seem version is already set in `BaseHTTPResponse` when it is initialized as an integer.

https://github.com/programmer-ke/urllib3/blob/03f7b65a47d87036674a7909ca6c57d4b5cf1a81/src/urllib3/response.py#L332
https://github.com/programmer-ke/urllib3/blob/03f7b65a47d87036674a7909ca6c57d4b5cf1a81/src/urllib3/connection.py#L484
https://docs.python.org/3/library/http.client.html#http.client.HTTPResponse.version
> Turns out `BaseHTTPResponse` already has a `version` field, which gets populated from `http.client.HTTPVersion.version`. It's an integer mapped from the [HTTP-version field](https://www.rfc-editor.org/rfc/rfc9112.html#name-http-version) in the start line. For example, `HTTP/1.1` is mapped to 11: https://github.com/python/cpython/blob/b822b85ac11e73bbe4417bf03ee770ab116bb42d/Lib/http/client.py#L342-L348.
> 
> While arguably more ugly than using the full string or even the [ALPN name](https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids), according to [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-protocol-version) it's OK to assume that a protocol will be two digits (major and minor), with the second digit being 0 if not specified.
> 
> Two decisions are needed here:
> 
>     1. Do we add a new `http_version` field and fill it with `HTTP/1.1` or `HTTP/2`?
> 
>     2. Do we keep the current `version` field and use 20 for HTTP/2?
> 
> 
> My loosely held opinion is 1. No 2. Yes.

This comment suggests that with emscripten, `_http_vsn_str` doesn't exist on the connection object.
https://github.com/urllib3/urllib3/blob/03f7b65a47d87036674a7909ca6c57d4b5cf1a81/src/urllib3/connectionpool.py#L547

 I'm not sure what the implications are when it comes to the version set in the `BaseHTTPResponse`. Maybe an edge case, but I'm not sure as I'm not familiar with the emscripten workings..
> This comment suggests that with emscripten, `_http_vsn_str` doesn't exist on the connection object.
> 
> https://github.com/urllib3/urllib3/blob/03f7b65a47d87036674a7909ca6c57d4b5cf1a81/src/urllib3/connectionpool.py#L547
> 
> I'm not sure what the implications are when it comes to the version set in the `BaseHTTPResponse`. Maybe an edge case, but I'm not sure as I'm not familiar with the emscripten workings..

Yeah, `HTTP2Connection` is a subclass of `http.client.HTTPConnection`, and the latter hard-codes `_http_vsn_str = 'HTTP/1.1'` which we use for logging here.

https://github.com/urllib3/urllib3/blob/71e7c35662a4b2ba8543194cc132616065dfc56a/src/urllib3/connectionpool.py#L547-L560

cc @pquentin @sethmlarson



Patch:
diff --git a/src/urllib3/http2.py b/src/urllib3/http2.py
index 3364500c48..05de106f73 100644
--- a/src/urllib3/http2.py
+++ b/src/urllib3/http2.py
@@ -10,6 +10,7 @@
 
 import urllib3.connection
 import urllib3.util.ssl_
+from urllib3.response import BaseHTTPResponse
 
 from ._collections import HTTPHeaderDict
 from .connection import HTTPSConnection
@@ -54,6 +55,7 @@ def putrequest(
         skip_accept_encoding: bool = False,
     ) -> None:
         with self._lock_h2_conn() as h2_conn:
+            self._request_url = url
             self._h2_stream = h2_conn.get_next_available_stream_id()
 
             if ":" in self.host:
@@ -134,7 +136,12 @@ def getresponse(  # type: ignore[override]
         self.close()
 
         assert status is not None
-        return HTTP2Response(status=status, headers=headers, data=bytes(data))
+        return HTTP2Response(
+            status=status,
+            headers=headers,
+            request_url=self._request_url,
+            data=bytes(data),
+        )
 
     def close(self) -> None:
         with self._lock_h2_conn() as h2_conn:
@@ -155,20 +162,39 @@ def close(self) -> None:
         super().close()
 
 
-class HTTP2Response:
+class HTTP2Response(BaseHTTPResponse):
     # TODO: This is a woefully incomplete response object, but works for non-streaming.
-    def __init__(self, status: int, headers: HTTPHeaderDict, data: bytes) -> None:
-        self.status = status
-        self.headers = headers
-        self.data = data
+    def __init__(
+        self,
+        status: int,
+        headers: HTTPHeaderDict,
+        request_url: str,
+        data: bytes,
+        decode_content: bool = False,  # TODO: support decoding
+    ) -> None:
+        super().__init__(
+            status=status,
+            headers=headers,
+            # Following CPython, we map HTTP versions to major * 10 + minor integers
+            version=20,
+            # No reason phrase in HTTP/2
+            reason=None,
+            decode_content=decode_content,
+            request_url=request_url,
+        )
+        self._data = data
         self.length_remaining = 0
 
+    @property
+    def data(self) -> bytes:
+        return self._data
+
     def get_redirect_location(self) -> None:
         return None
 
 
 def inject_into_urllib3() -> None:
-    HTTPSConnectionPool.ConnectionCls = HTTP2Connection  # type: ignore[assignment]
+    HTTPSConnectionPool.ConnectionCls = HTTP2Connection
     urllib3.connection.HTTPSConnection = HTTP2Connection  # type: ignore[misc]
 
     # TODO: Offer 'http/1.1' as well, but for testing purposes this is handy.


Test patch:
diff --git a/test/with_dummyserver/test_https.py b/test/with_dummyserver/test_https.py
index 3ec313699c..aa22f11879 100644
--- a/test/with_dummyserver/test_https.py
+++ b/test/with_dummyserver/test_https.py
@@ -139,6 +139,7 @@ def test_simple(self, http_version: str) -> None:
             r = https_pool.request("GET", "/")
             assert r.status == 200, r.data
             assert r.headers["server"] == f"hypercorn-{http_version}"
+            assert r.data == b"Dummy server!"
 
     @resolvesLocalhostFQDN()
     def test_dotted_fqdn(self) -> None: