Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HTTP3 support #829

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions httpcore/__init__.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
AsyncConnectionInterface, AsyncConnectionInterface,
AsyncConnectionPool, AsyncConnectionPool,
AsyncHTTP2Connection, AsyncHTTP2Connection,
AsyncHTTP3Connection,
AsyncHTTP11Connection, AsyncHTTP11Connection,
AsyncHTTPConnection, AsyncHTTPConnection,
AsyncHTTPProxy, AsyncHTTPProxy,
Expand Down Expand Up @@ -40,6 +41,7 @@
ConnectionInterface, ConnectionInterface,
ConnectionPool, ConnectionPool,
HTTP2Connection, HTTP2Connection,
HTTP3Connection,
HTTP11Connection, HTTP11Connection,
HTTPConnection, HTTPConnection,
HTTPProxy, HTTPProxy,
Expand Down Expand Up @@ -85,6 +87,7 @@ def __init__(self, *args, **kwargs): # type: ignore
"AsyncHTTPProxy", "AsyncHTTPProxy",
"AsyncHTTP11Connection", "AsyncHTTP11Connection",
"AsyncHTTP2Connection", "AsyncHTTP2Connection",
"AsyncHTTP3Connection",
"AsyncConnectionInterface", "AsyncConnectionInterface",
"AsyncSOCKSProxy", "AsyncSOCKSProxy",
# sync # sync
Expand All @@ -93,6 +96,7 @@ def __init__(self, *args, **kwargs): # type: ignore
"HTTPProxy", "HTTPProxy",
"HTTP11Connection", "HTTP11Connection",
"HTTP2Connection", "HTTP2Connection",
"HTTP3Connection",
"ConnectionInterface", "ConnectionInterface",
"SOCKSProxy", "SOCKSProxy",
# network backends, implementations # network backends, implementations
Expand Down
13 changes: 13 additions & 0 deletions httpcore/_async/__init__.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ def __init__(self, *args, **kwargs) -> None: # type: ignore
) )




try:
from .http3 import AsyncHTTP3Connection
except ImportError: # pragma: nocover

class AsyncHTTP3Connection: # type: ignore
def __init__(self, *args, **kwargs) -> None: # type: ignore
raise RuntimeError(
"Attempted to use http3 support, but the `aioquic` package is not "
"installed. Use 'pip install httpcore[http3]'."
)


try: try:
from .socks_proxy import AsyncSOCKSProxy from .socks_proxy import AsyncSOCKSProxy
except ImportError: # pragma: nocover except ImportError: # pragma: nocover
Expand All @@ -34,6 +46,7 @@ def __init__(self, *args, **kwargs) -> None: # type: ignore
"AsyncHTTPProxy", "AsyncHTTPProxy",
"AsyncHTTP11Connection", "AsyncHTTP11Connection",
"AsyncHTTP2Connection", "AsyncHTTP2Connection",
"AsyncHTTP3Connection",
"AsyncConnectionInterface", "AsyncConnectionInterface",
"AsyncSOCKSProxy", "AsyncSOCKSProxy",
] ]
39 changes: 38 additions & 1 deletion httpcore/_async/connection.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def __init__(
keepalive_expiry: Optional[float] = None, keepalive_expiry: Optional[float] = None,
http1: bool = True, http1: bool = True,
http2: bool = False, http2: bool = False,
http3: bool = False,
retries: int = 0, retries: int = 0,
local_address: Optional[str] = None, local_address: Optional[str] = None,
uds: Optional[str] = None, uds: Optional[str] = None,
Expand All @@ -52,6 +53,7 @@ def __init__(
self._keepalive_expiry = keepalive_expiry self._keepalive_expiry = keepalive_expiry
self._http1 = http1 self._http1 = http1
self._http2 = http2 self._http2 = http2
self._http3 = http3
self._retries = retries self._retries = retries
self._local_address = local_address self._local_address = local_address
self._uds = uds self._uds = uds
Expand Down Expand Up @@ -80,7 +82,18 @@ async def handle_async_request(self, request: Request) -> Response:
ssl_object is not None ssl_object is not None
and ssl_object.selected_alpn_protocol() == "h2" and ssl_object.selected_alpn_protocol() == "h2"
) )
if http2_negotiated or (self._http2 and not self._http1): if self._http3 and not (
self._http1 or self._http2
): # pragma: no cover
karpetrosyan marked this conversation as resolved.
Show resolved Hide resolved
from .http3 import AsyncHTTP3Connection
karpetrosyan marked this conversation as resolved.
Show resolved Hide resolved

stream = await self._connect_http3(request)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be doing happy eyeballs

self._connection = AsyncHTTP3Connection(
origin=self._origin,
stream=stream,
keepalive_expiry=self._keepalive_expiry,
)
elif http2_negotiated or (self._http2 and not self._http1):
from .http2 import AsyncHTTP2Connection from .http2 import AsyncHTTP2Connection


self._connection = AsyncHTTP2Connection( self._connection = AsyncHTTP2Connection(
Expand Down Expand Up @@ -162,6 +175,30 @@ async def _connect(self, request: Request) -> AsyncNetworkStream:
async with Trace("retry", logger, request, kwargs) as trace: async with Trace("retry", logger, request, kwargs) as trace:
await self._network_backend.sleep(delay) await self._network_backend.sleep(delay)


async def _connect_http3(
self, request: Request
) -> AsyncNetworkStream: # pragma: nocover
retries_left = self._retries
delays = exponential_backoff(factor=RETRIES_BACKOFF_FACTOR)

while True:
try:
kwargs = {
"host": self._origin.host.decode("ascii"),
"port": self._origin.port,
}
async with Trace("connect_udp", logger, request, kwargs) as trace:
stream = await self._network_backend.connect_udp(**kwargs) # type: ignore
trace.return_value = stream
return stream
except (ConnectError, ConnectTimeout):
if retries_left <= 0:
raise
retries_left -= 1
delay = next(delays)
async with Trace("retry", logger, request, kwargs) as trace:
await self._network_backend.sleep(delay)

def can_handle_request(self, origin: Origin) -> bool: def can_handle_request(self, origin: Origin) -> bool:
return origin == self._origin return origin == self._origin


Expand Down
5 changes: 5 additions & 0 deletions httpcore/_async/connection_pool.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def __init__(
keepalive_expiry: Optional[float] = None, keepalive_expiry: Optional[float] = None,
http1: bool = True, http1: bool = True,
http2: bool = False, http2: bool = False,
http3: bool = False,
retries: int = 0, retries: int = 0,
local_address: Optional[str] = None, local_address: Optional[str] = None,
uds: Optional[str] = None, uds: Optional[str] = None,
Expand All @@ -77,6 +78,8 @@ def __init__(
by the connection pool. Defaults to True. by the connection pool. Defaults to True.
http2: A boolean indicating if HTTP/2 requests should be supported by http2: A boolean indicating if HTTP/2 requests should be supported by
the connection pool. Defaults to False. the connection pool. Defaults to False.
http3: A boolean indicating if HTTP/3 requests should be supported by
the connection pool. Defaults to False.
retries: The maximum number of retries when trying to establish a retries: The maximum number of retries when trying to establish a
connection. connection.
local_address: Local address to connect from. Can also be used to connect local_address: Local address to connect from. Can also be used to connect
Expand Down Expand Up @@ -105,6 +108,7 @@ def __init__(
self._keepalive_expiry = keepalive_expiry self._keepalive_expiry = keepalive_expiry
self._http1 = http1 self._http1 = http1
self._http2 = http2 self._http2 = http2
self._http3 = http3
self._retries = retries self._retries = retries
self._local_address = local_address self._local_address = local_address
self._uds = uds self._uds = uds
Expand All @@ -131,6 +135,7 @@ def create_connection(self, origin: Origin) -> AsyncConnectionInterface:
keepalive_expiry=self._keepalive_expiry, keepalive_expiry=self._keepalive_expiry,
http1=self._http1, http1=self._http1,
http2=self._http2, http2=self._http2,
http3=self._http3,
retries=self._retries, retries=self._retries,
local_address=self._local_address, local_address=self._local_address,
uds=self._uds, uds=self._uds,
Expand Down
Loading
Loading