Skip to content

Commit c599e46

Browse files
HTTP/2 prior knowledge (#333)
* introduce http2_prior_knowledge option for http connections * add commentary to http2_prior_knowledge var * unasync http2_prior_knowledge code * add http2 enforcing in alpn negociation * revert proxy changes for http2_prior_knowledge * use http1 boolean instead of http2_prior_knowledge variable * remove RemoteProtocolError when finding http1 string in http2 payload * Tweak HTTP/2 prior knowledge support * Tweak whitespacing Co-authored-by: avy <alexandre.vaney@p1sec.com>
1 parent 5809b95 commit c599e46

6 files changed

Lines changed: 50 additions & 10 deletions

File tree

httpcore/_async/connection.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from ssl import SSLContext
2-
from typing import Optional, Tuple, cast
2+
from typing import List, Optional, Tuple, cast
33

44
from .._backends.auto import AsyncBackend, AsyncLock, AsyncSocketStream, AutoBackend
55
from .._exceptions import ConnectError, ConnectTimeout
@@ -23,6 +23,7 @@ class AsyncHTTPConnection(AsyncHTTPTransport):
2323
def __init__(
2424
self,
2525
origin: Origin,
26+
http1: bool = True,
2627
http2: bool = False,
2728
uds: str = None,
2829
ssl_context: SSLContext = None,
@@ -32,15 +33,21 @@ def __init__(
3233
backend: AsyncBackend = None,
3334
):
3435
self.origin = origin
36+
self.http1 = http1
3537
self.http2 = http2
3638
self.uds = uds
3739
self.ssl_context = SSLContext() if ssl_context is None else ssl_context
3840
self.socket = socket
3941
self.local_address = local_address
4042
self.retries = retries
4143

42-
if self.http2:
43-
self.ssl_context.set_alpn_protocols(["http/1.1", "h2"])
44+
alpn_protocols: List[str] = []
45+
if http1:
46+
alpn_protocols.append("http/1.1")
47+
if http2:
48+
alpn_protocols.append("h2")
49+
50+
self.ssl_context.set_alpn_protocols(alpn_protocols)
4451

4552
self.connection: Optional[AsyncBaseHTTPConnection] = None
4653
self.is_http11 = False
@@ -147,7 +154,7 @@ def _create_connection(self, socket: AsyncSocketStream) -> None:
147154
logger.trace(
148155
"create_connection socket=%r http_version=%r", socket, http_version
149156
)
150-
if http_version == "HTTP/2":
157+
if http_version == "HTTP/2" or (self.http2 and not self.http1):
151158
from .http2 import AsyncHTTP2Connection
152159

153160
self.is_http2 = True

httpcore/_async/connection_pool.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,10 @@ class AsyncConnectionPool(AsyncHTTPTransport):
8989
connections.
9090
keepalive_expiry:
9191
The maximum time to allow before closing a keep-alive connection.
92+
http1:
93+
Enable/Disable HTTP/1.1 support. Defaults to True.
9294
http2:
93-
Enable HTTP/2 support.
95+
Enable/Disable HTTP/2 support. Defaults to False.
9496
uds:
9597
Path to a Unix Domain Socket to use instead of TCP sockets.
9698
local_address:
@@ -110,6 +112,7 @@ def __init__(
110112
max_connections: int = None,
111113
max_keepalive_connections: int = None,
112114
keepalive_expiry: float = None,
115+
http1: bool = True,
113116
http2: bool = False,
114117
uds: str = None,
115118
local_address: str = None,
@@ -131,6 +134,7 @@ def __init__(
131134
self._max_connections = max_connections
132135
self._max_keepalive_connections = max_keepalive_connections
133136
self._keepalive_expiry = keepalive_expiry
137+
self._http1 = http1
134138
self._http2 = http2
135139
self._uds = uds
136140
self._local_address = local_address
@@ -140,6 +144,9 @@ def __init__(
140144
self._backend = backend
141145
self._next_keepalive_check = 0.0
142146

147+
if not (http1 or http2):
148+
raise ValueError("Either http1 or http2 must be True.")
149+
143150
if http2:
144151
try:
145152
import h2 # noqa: F401
@@ -175,6 +182,7 @@ def _create_connection(
175182
) -> AsyncHTTPConnection:
176183
return AsyncHTTPConnection(
177184
origin=origin,
185+
http1=self._http1,
178186
http2=self._http2,
179187
uds=self._uds,
180188
ssl_context=self._ssl_context,

httpcore/_sync/connection.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from ssl import SSLContext
2-
from typing import Optional, Tuple, cast
2+
from typing import List, Optional, Tuple, cast
33

44
from .._backends.sync import SyncBackend, SyncLock, SyncSocketStream, SyncBackend
55
from .._exceptions import ConnectError, ConnectTimeout
@@ -23,6 +23,7 @@ class SyncHTTPConnection(SyncHTTPTransport):
2323
def __init__(
2424
self,
2525
origin: Origin,
26+
http1: bool = True,
2627
http2: bool = False,
2728
uds: str = None,
2829
ssl_context: SSLContext = None,
@@ -32,15 +33,21 @@ def __init__(
3233
backend: SyncBackend = None,
3334
):
3435
self.origin = origin
36+
self.http1 = http1
3537
self.http2 = http2
3638
self.uds = uds
3739
self.ssl_context = SSLContext() if ssl_context is None else ssl_context
3840
self.socket = socket
3941
self.local_address = local_address
4042
self.retries = retries
4143

42-
if self.http2:
43-
self.ssl_context.set_alpn_protocols(["http/1.1", "h2"])
44+
alpn_protocols: List[str] = []
45+
if http1:
46+
alpn_protocols.append("http/1.1")
47+
if http2:
48+
alpn_protocols.append("h2")
49+
50+
self.ssl_context.set_alpn_protocols(alpn_protocols)
4451

4552
self.connection: Optional[SyncBaseHTTPConnection] = None
4653
self.is_http11 = False
@@ -147,7 +154,7 @@ def _create_connection(self, socket: SyncSocketStream) -> None:
147154
logger.trace(
148155
"create_connection socket=%r http_version=%r", socket, http_version
149156
)
150-
if http_version == "HTTP/2":
157+
if http_version == "HTTP/2" or (self.http2 and not self.http1):
151158
from .http2 import SyncHTTP2Connection
152159

153160
self.is_http2 = True

httpcore/_sync/connection_pool.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,10 @@ class SyncConnectionPool(SyncHTTPTransport):
8989
connections.
9090
keepalive_expiry:
9191
The maximum time to allow before closing a keep-alive connection.
92+
http1:
93+
Enable/Disable HTTP/1.1 support. Defaults to True.
9294
http2:
93-
Enable HTTP/2 support.
95+
Enable/Disable HTTP/2 support. Defaults to False.
9496
uds:
9597
Path to a Unix Domain Socket to use instead of TCP sockets.
9698
local_address:
@@ -110,6 +112,7 @@ def __init__(
110112
max_connections: int = None,
111113
max_keepalive_connections: int = None,
112114
keepalive_expiry: float = None,
115+
http1: bool = True,
113116
http2: bool = False,
114117
uds: str = None,
115118
local_address: str = None,
@@ -131,6 +134,7 @@ def __init__(
131134
self._max_connections = max_connections
132135
self._max_keepalive_connections = max_keepalive_connections
133136
self._keepalive_expiry = keepalive_expiry
137+
self._http1 = http1
134138
self._http2 = http2
135139
self._uds = uds
136140
self._local_address = local_address
@@ -140,6 +144,9 @@ def __init__(
140144
self._backend = backend
141145
self._next_keepalive_check = 0.0
142146

147+
if not (http1 or http2):
148+
raise ValueError("Either http1 or http2 must be True.")
149+
143150
if http2:
144151
try:
145152
import h2 # noqa: F401
@@ -175,6 +182,7 @@ def _create_connection(
175182
) -> SyncHTTPConnection:
176183
return SyncHTTPConnection(
177184
origin=origin,
185+
http1=self._http1,
178186
http2=self._http2,
179187
uds=self._uds,
180188
ssl_context=self._ssl_context,

tests/async_tests/test_interfaces.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ async def read_body(stream: httpcore.AsyncByteStream) -> bytes:
2323
await stream.aclose()
2424

2525

26+
def test_must_configure_either_http1_or_http2() -> None:
27+
with pytest.raises(ValueError):
28+
httpcore.AsyncConnectionPool(http1=False, http2=False)
29+
30+
2631
@pytest.mark.anyio
2732
async def test_http_request(backend: str, server: Server) -> None:
2833
async with httpcore.AsyncConnectionPool(backend=backend) as http:

tests/sync_tests/test_interfaces.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ def read_body(stream: httpcore.SyncByteStream) -> bytes:
2323
stream.close()
2424

2525

26+
def test_must_configure_either_http1_or_http2() -> None:
27+
with pytest.raises(ValueError):
28+
httpcore.SyncConnectionPool(http1=False, http2=False)
29+
30+
2631

2732
def test_http_request(backend: str, server: Server) -> None:
2833
with httpcore.SyncConnectionPool(backend=backend) as http:

0 commit comments

Comments
 (0)