diff --git a/tools/webtransport/h3/webtransport_h3_server.py b/tools/webtransport/h3/webtransport_h3_server.py index 623a534b95576f..2dd8f645551d63 100644 --- a/tools/webtransport/h3/webtransport_h3_server.py +++ b/tools/webtransport/h3/webtransport_h3_server.py @@ -1,6 +1,7 @@ # mypy: allow-subclassing-any, no-warn-return-any import asyncio +import contextlib import logging import os import ssl @@ -9,17 +10,21 @@ import traceback from enum import IntEnum from urllib.parse import urlparse -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, cast # TODO(bashi): Remove import check suppressions once aioquic dependency is resolved. from aioquic.buffer import Buffer # type: ignore from aioquic.asyncio import QuicConnectionProtocol, serve # type: ignore from aioquic.asyncio.client import connect # type: ignore +from aioquic.asyncio.protocol import QuicStreamAdapter # type: ignore from aioquic.h3.connection import H3_ALPN, FrameType, H3Connection, ProtocolError, SettingsError # type: ignore from aioquic.h3.events import H3Event, HeadersReceived, WebTransportStreamDataReceived, DatagramReceived, DataReceived # type: ignore from aioquic.quic.configuration import QuicConfiguration # type: ignore from aioquic.quic.connection import logger as quic_connection_logger # type: ignore -from aioquic.quic.connection import stream_is_unidirectional +from aioquic.quic.connection import ( + stream_is_client_initiated, + stream_is_unidirectional, +) from aioquic.quic.events import QuicEvent, ProtocolNegotiated, ConnectionTerminated, StreamReset # type: ignore from aioquic.tls import SessionTicket # type: ignore @@ -602,6 +607,25 @@ async def _connect_server_with_timeout(host: str, port: int, timeout: float) -> return True +def _close_unusable_writer(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: + # Starting in python3.11, `StreamWriter.__del__` implicitly `close()`s + # itself [0], if it has not done so already. Because aioquic sometimes + # models a unidirectional stream with a bidirectional transport [1], the + # `writer` here for a server -> client stream may log a benign but + # scary-looking exception when it's GC'ed and unsuccessfully writes EOF. + # + # For the purpose of checking connectivity, work around this for now by + # preemptively closing such streams and suppressing the resulting + # exceptions. + # + # [0]: https://github.com/python/cpython/blob/3.11/Lib/asyncio/streams.py#L413 + # [1]: https://github.com/aiortc/aioquic/blob/1.2.0/src/aioquic/asyncio/protocol.py#L241 + stream_id = cast(QuicStreamAdapter, writer.transport).stream_id + if stream_is_unidirectional(stream_id) and not stream_is_client_initiated(stream_id): + with contextlib.suppress(ValueError): + writer.close() + + async def _connect_to_server(host: str, port: int) -> None: configuration = QuicConfiguration( alpn_protocols=H3_ALPN, @@ -609,5 +633,6 @@ async def _connect_to_server(host: str, port: int) -> None: verify_mode=ssl.CERT_NONE, ) - async with connect(host, port, configuration=configuration) as protocol: + async with connect(host, port, configuration=configuration, + stream_handler=_close_unusable_writer) as protocol: await protocol.ping()