From e659b64bf042e2477ade6ba6e4be222050a5c0b4 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Tue, 9 Jun 2026 23:55:59 +0800 Subject: [PATCH] Add async_iterable_to_stream utility to avoid aiohttp compatibility issues Create async_iterable_to_stream() in gns3server.utils.asyncio that converts an async iterable to an aiohttp StreamReader via a background feeder task. This bypasses aiohttp's AsyncIterablePayload which can cause 'Connection reset by peer' with certain HTTP servers. Use it in _run_http_query for the __aiter__ data path. --- gns3server/controller/compute.py | 3 +- gns3server/utils/asyncio/__init__.py | 41 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/gns3server/controller/compute.py b/gns3server/controller/compute.py index d898cf6da..7ec8691a8 100644 --- a/gns3server/controller/compute.py +++ b/gns3server/controller/compute.py @@ -32,7 +32,7 @@ else: from async_timeout import timeout as asynctimeout from ..utils import parse_version -from ..utils.asyncio import locking +from ..utils.asyncio import locking, async_iterable_to_stream from ..controller.controller_error import ( ControllerError, ControllerBadRequestError, @@ -537,6 +537,7 @@ class Compute: elif hasattr(data, "__aiter__"): chunked = True headers["content-type"] = "application/octet-stream" + data = await async_iterable_to_stream(data) # If the data is an open file we will iterate on it elif isinstance(data, io.BufferedIOBase): chunked = True diff --git a/gns3server/utils/asyncio/__init__.py b/gns3server/utils/asyncio/__init__.py index 34ae5d441..1b9e443a3 100644 --- a/gns3server/utils/asyncio/__init__.py +++ b/gns3server/utils/asyncio/__init__.py @@ -136,3 +136,44 @@ def locking(f): return await f(oself, *args, **kwargs) return wrapper + + +async def async_iterable_to_stream(async_iter, limit=65536): + """ + Convert an async iterable into an aiohttp StreamReader. + + This avoids passing async generators directly to aiohttp's payload + system, which can cause compatibility issues with certain HTTP servers. + + :param async_iter: An async iterable that yields bytes + :param limit: Buffer limit for the StreamReader (default 64KB) + :returns: aiohttp.streams.StreamReader + """ + + from aiohttp.streams import StreamReader + + class _NoopProtocol: + _reading_paused = False + connected = True + + def pause_reading(self): + self._reading_paused = True + + def resume_reading(self, resume_parser=False): + self._reading_paused = False + + reader = StreamReader(_NoopProtocol(), limit=limit) + + async def _feed(): + try: + async for chunk in async_iter: + reader.feed_data(chunk) + except GeneratorExit: + raise + except Exception: + pass + finally: + reader.feed_eof() + + asyncio.ensure_future(_feed()) + return reader