-
Notifications
You must be signed in to change notification settings - Fork 106
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
base: master
Are you sure you want to change the base?
Add HTTP3 support #829
Conversation
e2af16a
to
8fc5ff3
Compare
8fc5ff3
to
bd31869
Compare
This is how I see the The goals here are:
I want to keep this pull request as simple as possible, but I'm also thinking about including That is:
|
Any thoughts or ideas, @encode/maintainers? |
Thanks @karpetrosyan!
Here's my initial high level thoughts...
|
Thank you for reviewing, Tom. Excellent questions; here are my thoughts on that. Review of the current landscape... Which sites currently use HTTP/3 and which browsers can you demonstrate using it? How can someone else observe this?As an example, here are a few large corporations that support HTTP/3: Using this script, you can already test it with httpcore. import httpcore
import logging
logging.basicConfig(level=1)
pool = httpcore.ConnectionPool(http1=False, http3=True)
websites = [
"https://google.com",
"https://youtube.com",
"https://instagram.com",
"https://spotify.com",
"https://cloudflare.com",
]
for website in websites:
response = pool.request("GET", website, extensions={"timeouts": {"connect": 2}})
print(response)
According to Wikipedia, all major browsers support the HTTP/3 protocol.
You can also use this website to determine whether the request was sent over HTTP/3 or HTTP/1.1, and then open the dev tool to view the schema and headers that were sent over the network. To learn more about HTTP/3 state in 2023, visit https://blog.cloudflare.com/http3-usage-one-year-on/. What's the use-case for HTTP/3 in httpx - are there conditions under which it's beneficial to the user?Here are some of the reasons why we should add HTTP/3 support.
How do we intend to maintain the HTTP/3 work alongside the existing HTTP/2 work with a minimal maintenance load?Yes, this is an important question. One of the goals was to implement HTTP/3 with as few differences as possible from HTTP/2. We can assume that What discovery mechanism are browsers currently using for HTTP/3 detection? Is detection over DNS records currently deployed and used?This question has already been discussed in encode/httpx#275. There is also a section in RFC that describes the connection setup process, so you can find more detailed information there. |
Would love to see HTTP3 support in httpx |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO when enabling multiple HTTP versions, could consider http3 to have higher precedence than two others, since it uses UDP, though could still fallback to TCP.
An alternative example for this change: can_connect_tcp = True
if self._http3:
try:
from . import AsyncHTTP3Connection
stream = await self._connect_http3(request)
self._connection = AsyncHTTP3Connection(
origin=self._origin,
stream=stream,
keepalive_expiry=self._keepalive_expiry,
)
can_connect_tcp = False
except Exception as exc:
if not (self._http1 or self._http2):
raise exc
if can_connect_tcp:
stream = await self._connect(request)
ssl_object = stream.get_extra_info("ssl_object")
http2_negotiated = (
ssl_object is not None
and ssl_object.selected_alpn_protocol() == "h2"
)
if http2_negotiated or (self._http2 and not self._http1):
from .http2 import AsyncHTTP2Connection
self._connection = AsyncHTTP2Connection(
origin=self._origin,
stream=stream,
keepalive_expiry=self._keepalive_expiry,
)
else:
self._connection = AsyncHTTP11Connection(
origin=self._origin,
stream=stream,
keepalive_expiry=self._keepalive_expiry,
) |
Ugh, let's consider next steps here. HTTP/3 NegotiationThere are at least three approaches we could take to solve this problem:
Let's go over each one and provide some useful links so you can dig deeper. Alt-Svc
HTTP servers, for example, frequently use this
In the world of Also, the server may provide additional information with the alternative service, such as an expiration time, which we must respect and avoid using stale information about the alternative service. Here is an example of an
This also complicates the use of this approach, so we should account for it. See also: https://http3-explained.haxx.se/en/h3/h3-altsvc
|
I'll leave some key differences between our NotesIf you are unfamiliar with HTTP/3 and HTTP/2, I recommend the following resources: In a nutshell, Unlike in One is the This separation can somewhat help the developer to distinguish the connection layer and the HTTP layer, so we have, for example, In The additional layer is also the reason why there are two "connection" objects in the aioquic package, because unlike in the http2 implementation, where we care only about tcp and http, now we should think about udp, quic, and http. The QUIC layer also helps us to get rid of flow window control, connection setup, and related things that are now handled by the QUIC protocol itself. ChangedEventsFirst, here is how we import HTTP2 events and HTTP3 events import h2.events
from aioquic.h3 import events as h3_events
from aioquic.quic import events as quic_events We handle five events in HTTP2 implementation; here is how those events look in HTTP3.
Here is the code reference for that part in the http3.py and http2.py RemovedMethod
|
Re DNS HTTPS records, dnspython has good support for them, but dnspython also uses httpx (and thus httpcore) for DNS-over-HTTPS, so I'm not completely sure how to deal with the chicken-and-egg mutual module dependency issues, but I'm happy to work with the httpcore team. |
Where is HTTP/3 support on the release schedule? |
}, | ||
) | ||
except BaseException as exc: # noqa: PIE786 | ||
with AsyncShieldCancellation(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might need changing wrt #927
I’m not sure why the pipeline failed, but the implementation works. I would like to continue working on this, and we need to cover the implementation with tests. What do you think, @encode/maintainers? Do we have any blockers? I would also appreciate a review from @jlaine, if possible. |
I can already see a 10-20% speed boost on my machine compared to our HTTP/2 implementation as well. |
I'm a bit concerned about how the pyopenssl context is configured. I think this would break Generally SSL in httpcore is configured by passing in a SSLContext but this PR seems to bypass that and pass certify.where() |
I think the way to do it is move |
I've been thinking about this for a while and using two different contexts for the same httpx session is cryptographically fishy (and probably slow - loading the cert store twice). I've had a quick look at the anyio trio and sync ssl streams and I'm happy to make a PR to make httpcore support either ssl or pyopenssl contexts then we can require a pyopenssl context for http3=True |
I've misunderstood how tls in aioquic works, I saw the dep on pyopenssl and made the assumption it's used for TLS. However aioquic uses it's own TLS 1.3 implementation which requires cadata, cafile, capath etc passed in, so we will need to create our own context that wraps the stdlib ssl context and the required parameters for aioquic |
Pointers? |
|
||
async def _do_handshake(self, request: Request) -> None: | ||
assert hasattr(self._network_stream, "_addr") | ||
self._quic_conn.connect(addr=self._network_stream._addr, now=monotonic()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should use the event loop time, so that trio can use auto jump clock
|
||
return events | ||
|
||
async def _write_outgoing_data(self, request: Request) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to call quic.get_timer() and scheduler a timer so that quic can queue lost datagrams
): # pragma: no cover | ||
from .http3 import AsyncHTTP3Connection | ||
|
||
stream = await self._connect_http3(request) |
There was a problem hiding this comment.
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
raise self._read_exception # pragma: nocover | ||
|
||
try: | ||
data = await self._network_stream.read(self.READ_NUM_BYTES, timeout) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you need a background task that's constantly reading any datagrams from the server as they can be sent unsolicited
This pull request tries to add HTTP/3 support.
As we know, the HTTP/2 and HTTP/3 protocols are very similar, except for the protocol they use.
This PR simply follows the steps described below.
connect_udp
method to "httpcore._backends.base.NetworkBackend".connect_udp
only for the synchronous backend (only for now).pyproject.toml
httpcore/_http3.py
fileHTTP/3
in that file, keeping the logic and flow maximum similar to the logic that we are using in _http2.py.To support the
HTTP/3
protocol, we need the aioquic package, which is a well-tested and well-designed implementation for theHTTP/3
andQUIC
protocols.For more details, see the issue in HTTPX, where the author of aioquic provides basic HTTP/3 integration for httpx.
There is a very basic example of how you can use HTTP/3 with the httpcore.
Or with the high-level API: