From 9c0705b4948237f91eba4b8fc4eadd27f1b90482 Mon Sep 17 00:00:00 2001 From: Ailitonia <41713304+ailitonia@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:58:39 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E4=B8=BA=E5=86=85=E7=BD=AE?= =?UTF-8?q?=E9=A9=B1=E5=8A=A8=E5=99=A8=E8=AF=B7=E6=B1=82=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20`auto=5Fredirects`=20=E5=8F=82=E6=95=B0,?= =?UTF-8?q?=20=E4=BB=A5=E4=BE=BF=E6=8E=A7=E5=88=B6=E9=87=8D=E5=AE=9A?= =?UTF-8?q?=E5=90=91=E8=A1=8C=E4=B8=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nonebot/drivers/aiohttp.py | 2 ++ nonebot/drivers/httpx.py | 2 ++ nonebot/drivers/httpx2.py | 2 ++ nonebot/internal/driver/model.py | 3 +++ 4 files changed, 9 insertions(+) diff --git a/nonebot/drivers/aiohttp.py b/nonebot/drivers/aiohttp.py index bc063cda0bb1..61bb54004b40 100644 --- a/nonebot/drivers/aiohttp.py +++ b/nonebot/drivers/aiohttp.py @@ -169,6 +169,7 @@ async def request(self, setup: Request) -> Response: headers=setup.headers, proxy=setup.proxy or self._proxy, timeout=self._get_timeout(setup.timeout), + allow_redirects=setup.auto_redirects, ) as response: return Response( response.status, @@ -210,6 +211,7 @@ async def stream_request( headers=setup.headers, proxy=setup.proxy or self._proxy, timeout=self._get_timeout(setup.timeout), + allow_redirects=setup.auto_redirects, ) as response: response_headers = response.headers.copy() # aiohttp does not guarantee fixed-size chunks; re-chunk to exact size diff --git a/nonebot/drivers/httpx.py b/nonebot/drivers/httpx.py index a8217b793c53..afca60b5b167 100644 --- a/nonebot/drivers/httpx.py +++ b/nonebot/drivers/httpx.py @@ -145,6 +145,7 @@ async def request(self, setup: Request) -> Response: headers=tuple(setup.headers.items()), cookies=setup.cookies.jar, timeout=self._get_timeout(setup.timeout), + follow_redirects=setup.auto_redirects, ) return Response( response.status_code, @@ -172,6 +173,7 @@ async def stream_request( headers=tuple(setup.headers.items()), cookies=setup.cookies.jar, timeout=self._get_timeout(setup.timeout), + follow_redirects=setup.auto_redirects, ) as response: response_headers = response.headers.multi_items() async for chunk in response.aiter_bytes(chunk_size=chunk_size): diff --git a/nonebot/drivers/httpx2.py b/nonebot/drivers/httpx2.py index a190d49bd59d..caa06c23155c 100644 --- a/nonebot/drivers/httpx2.py +++ b/nonebot/drivers/httpx2.py @@ -145,6 +145,7 @@ async def request(self, setup: Request) -> Response: headers=tuple(setup.headers.items()), cookies=setup.cookies.jar, timeout=self._get_timeout(setup.timeout), + follow_redirects=setup.auto_redirects, ) return Response( response.status_code, @@ -172,6 +173,7 @@ async def stream_request( headers=tuple(setup.headers.items()), cookies=setup.cookies.jar, timeout=self._get_timeout(setup.timeout), + follow_redirects=setup.auto_redirects, ) as response: response_headers = response.headers.multi_items() async for chunk in response.aiter_bytes(chunk_size=chunk_size): diff --git a/nonebot/internal/driver/model.py b/nonebot/internal/driver/model.py index f4fb0b4a9d8f..053fe8631d37 100644 --- a/nonebot/internal/driver/model.py +++ b/nonebot/internal/driver/model.py @@ -79,6 +79,7 @@ def __init__( timeout: TimeoutTypes | UnsetType = UNSET, proxy: str | None = None, ping_interval: PingIntervalTypes | UnsetType = UNSET, + auto_redirects: bool = True, ): # method self.method: str = ( @@ -94,6 +95,8 @@ def __init__( self.proxy: str | None = proxy # ping interval self.ping_interval: PingIntervalTypes | UnsetType = ping_interval + # auto redirects + self.auto_redirects: bool = auto_redirects # url if isinstance(url, tuple): From e0670da81a9f99c3a3894f0207942d9b806fb85d Mon Sep 17 00:00:00 2001 From: Ailitonia <41713304+ailitonia@users.noreply.github.com> Date: Sat, 26 Sep 2026 20:15:36 +0800 Subject: [PATCH 2/3] =?UTF-8?q?test:=20=E6=B7=BB=E5=8A=A0=E5=86=85?= =?UTF-8?q?=E7=BD=AE=E9=A9=B1=E5=8A=A8=E5=99=A8=E8=AF=B7=E6=B1=82=E9=87=8D?= =?UTF-8?q?=E5=AE=9A=E5=90=91=E8=A1=8C=E4=B8=BA=E6=B5=8B=E8=AF=95=E7=94=A8?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/fake_server.py | 6 +++ tests/test_driver.py | 118 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/tests/fake_server.py b/tests/fake_server.py index ea473286675c..bafacb4b80ad 100644 --- a/tests/fake_server.py +++ b/tests/fake_server.py @@ -79,6 +79,10 @@ def http_echo(request: Request) -> Response: ) +def http_redirect(request: Request) -> Response: + return Response("redirecting", status=302, headers={"Location": "/"}) + + def websocket_echo(request: Request) -> Response: stream = request.environ["werkzeug.socket"] @@ -137,5 +141,7 @@ def websocket_echo(request: Request) -> Response: def request_handler(request: Request) -> Response: if request.headers.get("Connection") == "Upgrade": return websocket_echo(request) + elif request.path == "/redirect": + return http_redirect(request) else: return http_echo(request) diff --git a/tests/test_driver.py b/tests/test_driver.py index 57930a4cb463..83cb404fc706 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -372,8 +372,8 @@ async def test_http_client(driver: Driver, server_url: URL): ) chunks = [] async for resp in driver.stream_request(request, chunk_size=4): - assert response.status_code == 200 - assert response.content + assert resp.status_code == 200 + assert resp.content chunks.append(resp.content) assert all(len(chunk) == 4 for chunk in chunks[:-1]) data = json.loads(b"".join(chunks)) @@ -388,6 +388,62 @@ async def test_http_client(driver: Driver, server_url: URL): await anyio.sleep(1) +@pytest.mark.anyio +@pytest.mark.parametrize( + "driver", + [ + pytest.param("nonebot.drivers.httpx:Driver", id="httpx"), + pytest.param("nonebot.drivers.httpx2:Driver", id="httpx2"), + pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"), + ], + indirect=True, +) +async def test_http_client_redirects(driver: Driver, server_url: URL): + assert isinstance(driver, HTTPClientMixin) + + redirect_url = server_url.join(URL("/redirect")) + + # follow redirects by default + request = Request("GET", redirect_url) + assert request.auto_redirects is True + response = await driver.request(request) + assert response.status_code == 200 + assert response.content + data = json.loads(response.content) + assert data["method"] == "GET" + assert URL(data["url"]).path == "/" + + # disable auto redirects + request = Request("GET", redirect_url, auto_redirects=False) + assert request.auto_redirects is False + response = await driver.request(request) + assert response.status_code == 302 + assert response.headers["Location"] == "/" + assert response.content == b"redirecting" + + # stream request following redirects + request = Request("GET", redirect_url) + chunks = [] + async for resp in driver.stream_request(request, chunk_size=4): + assert resp.status_code == 200 + assert resp.content + chunks.append(resp.content) + data = json.loads(b"".join(chunks)) + assert data["method"] == "GET" + assert URL(data["url"]).path == "/" + + # stream request without auto redirects + request = Request("GET", redirect_url, auto_redirects=False) + chunks = [] + async for resp in driver.stream_request(request, chunk_size=4): + assert resp.status_code == 302 + assert resp.content + chunks.append(resp.content) + assert b"".join(chunks) == b"redirecting" + + await anyio.sleep(1) + + @pytest.mark.anyio @pytest.mark.parametrize( "driver", @@ -600,6 +656,64 @@ async def test_http_client_session(driver: Driver, server_url: URL): await anyio.sleep(1) +@pytest.mark.anyio +@pytest.mark.parametrize( + "driver", + [ + pytest.param("nonebot.drivers.httpx:Driver", id="httpx"), + pytest.param("nonebot.drivers.httpx2:Driver", id="httpx2"), + pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"), + ], + indirect=True, +) +async def test_http_client_session_redirects(driver: Driver, server_url: URL): + assert isinstance(driver, HTTPClientMixin) + + redirect_url = server_url.join(URL("/redirect")) + + session = driver.get_session() + async with session: + # follow redirects by default + request = Request("GET", redirect_url) + assert request.auto_redirects is True + response = await session.request(request) + assert response.status_code == 200 + assert response.content + data = json.loads(response.content) + assert data["method"] == "GET" + assert URL(data["url"]).path == "/" + + # disable auto redirects + request = Request("GET", redirect_url, auto_redirects=False) + assert request.auto_redirects is False + response = await session.request(request) + assert response.status_code == 302 + assert response.headers["Location"] == "/" + assert response.content == b"redirecting" + + # stream request following redirects + request = Request("GET", redirect_url) + chunks = [] + async for resp in session.stream_request(request, chunk_size=4): + assert resp.status_code == 200 + assert resp.content + chunks.append(resp.content) + data = json.loads(b"".join(chunks)) + assert data["method"] == "GET" + assert URL(data["url"]).path == "/" + + # stream request without auto redirects + request = Request("GET", redirect_url, auto_redirects=False) + chunks = [] + async for resp in session.stream_request(request, chunk_size=4): + assert resp.status_code == 302 + assert resp.content + chunks.append(resp.content) + assert b"".join(chunks) == b"redirecting" + + await anyio.sleep(1) + + @pytest.mark.anyio async def test_aiohttp_stream_request_skip_empty_chunk() -> None: class _FakeContent: From 6af707045d3ef666b352b093c711832f8bdd5002 Mon Sep 17 00:00:00 2001 From: Ailitonia <41713304+ailitonia@users.noreply.github.com> Date: Sun, 27 Sep 2026 10:56:59 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E5=B0=86=E5=86=85=E7=BD=AE?= =?UTF-8?q?=E9=A9=B1=E5=8A=A8=E5=99=A8=E8=AF=B7=E6=B1=82=E6=96=B9=E6=B3=95?= =?UTF-8?q?=20`auto=5Fredirects`=20=E5=8F=82=E6=95=B0=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=80=BC=E8=B0=83=E6=95=B4=E4=B8=BA=20`UNSET`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nonebot/drivers/aiohttp.py | 10 ++++++++-- nonebot/drivers/httpx.py | 4 ++-- nonebot/drivers/httpx2.py | 4 ++-- nonebot/drivers/websockets.py | 6 ++++++ nonebot/internal/driver/model.py | 4 ++-- tests/test_driver.py | 4 ++-- 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/nonebot/drivers/aiohttp.py b/nonebot/drivers/aiohttp.py index 61bb54004b40..65a30afb5307 100644 --- a/nonebot/drivers/aiohttp.py +++ b/nonebot/drivers/aiohttp.py @@ -169,7 +169,7 @@ async def request(self, setup: Request) -> Response: headers=setup.headers, proxy=setup.proxy or self._proxy, timeout=self._get_timeout(setup.timeout), - allow_redirects=setup.auto_redirects, + **exclude_unset({"allow_redirects": setup.auto_redirects}), ) as response: return Response( response.status, @@ -211,7 +211,7 @@ async def stream_request( headers=setup.headers, proxy=setup.proxy or self._proxy, timeout=self._get_timeout(setup.timeout), - allow_redirects=setup.auto_redirects, + **exclude_unset({"allow_redirects": setup.auto_redirects}), ) as response: response_headers = response.headers.copy() # aiohttp does not guarantee fixed-size chunks; re-chunk to exact size @@ -337,6 +337,12 @@ async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]: "the configured ping timeout will be ignored." ) + if setup.auto_redirects is not UNSET: + logger.warning( + "aiohttp driver websocket method does not expose redirects parameter; " + "the configured auto_redirects will be ignored." + ) + async with aiohttp.ClientSession(version=version, trust_env=True) as session: async with session.ws_connect( setup.url, diff --git a/nonebot/drivers/httpx.py b/nonebot/drivers/httpx.py index afca60b5b167..128b1d37500d 100644 --- a/nonebot/drivers/httpx.py +++ b/nonebot/drivers/httpx.py @@ -145,7 +145,7 @@ async def request(self, setup: Request) -> Response: headers=tuple(setup.headers.items()), cookies=setup.cookies.jar, timeout=self._get_timeout(setup.timeout), - follow_redirects=setup.auto_redirects, + **exclude_unset({"follow_redirects": setup.auto_redirects}), ) return Response( response.status_code, @@ -173,7 +173,7 @@ async def stream_request( headers=tuple(setup.headers.items()), cookies=setup.cookies.jar, timeout=self._get_timeout(setup.timeout), - follow_redirects=setup.auto_redirects, + **exclude_unset({"follow_redirects": setup.auto_redirects}), ) as response: response_headers = response.headers.multi_items() async for chunk in response.aiter_bytes(chunk_size=chunk_size): diff --git a/nonebot/drivers/httpx2.py b/nonebot/drivers/httpx2.py index caa06c23155c..b78075630b4e 100644 --- a/nonebot/drivers/httpx2.py +++ b/nonebot/drivers/httpx2.py @@ -145,7 +145,7 @@ async def request(self, setup: Request) -> Response: headers=tuple(setup.headers.items()), cookies=setup.cookies.jar, timeout=self._get_timeout(setup.timeout), - follow_redirects=setup.auto_redirects, + **exclude_unset({"follow_redirects": setup.auto_redirects}), ) return Response( response.status_code, @@ -173,7 +173,7 @@ async def stream_request( headers=tuple(setup.headers.items()), cookies=setup.cookies.jar, timeout=self._get_timeout(setup.timeout), - follow_redirects=setup.auto_redirects, + **exclude_unset({"follow_redirects": setup.auto_redirects}), ) as response: response_headers = response.headers.multi_items() async for chunk in response.aiter_bytes(chunk_size=chunk_size): diff --git a/nonebot/drivers/websockets.py b/nonebot/drivers/websockets.py index 0580d6eee24c..d1dde5fcf39f 100644 --- a/nonebot/drivers/websockets.py +++ b/nonebot/drivers/websockets.py @@ -111,6 +111,12 @@ async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]: } ) + if setup.auto_redirects is not UNSET: + logger.warning( + "websockets driver websocket method does not expose " + "redirects parameter; the configured auto_redirects will be ignored." + ) + connection = connect( str(setup.url), additional_headers={**setup.headers, **setup.cookies.as_header(setup)}, diff --git a/nonebot/internal/driver/model.py b/nonebot/internal/driver/model.py index 053fe8631d37..7385557ff6d2 100644 --- a/nonebot/internal/driver/model.py +++ b/nonebot/internal/driver/model.py @@ -79,7 +79,7 @@ def __init__( timeout: TimeoutTypes | UnsetType = UNSET, proxy: str | None = None, ping_interval: PingIntervalTypes | UnsetType = UNSET, - auto_redirects: bool = True, + auto_redirects: bool | UnsetType = UNSET, ): # method self.method: str = ( @@ -96,7 +96,7 @@ def __init__( # ping interval self.ping_interval: PingIntervalTypes | UnsetType = ping_interval # auto redirects - self.auto_redirects: bool = auto_redirects + self.auto_redirects: bool | UnsetType = auto_redirects # url if isinstance(url, tuple): diff --git a/tests/test_driver.py b/tests/test_driver.py index 83cb404fc706..71973ed31c3e 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -405,7 +405,7 @@ async def test_http_client_redirects(driver: Driver, server_url: URL): # follow redirects by default request = Request("GET", redirect_url) - assert request.auto_redirects is True + assert request.auto_redirects is UNSET response = await driver.request(request) assert response.status_code == 200 assert response.content @@ -675,7 +675,7 @@ async def test_http_client_session_redirects(driver: Driver, server_url: URL): async with session: # follow redirects by default request = Request("GET", redirect_url) - assert request.auto_redirects is True + assert request.auto_redirects is UNSET response = await session.request(request) assert response.status_code == 200 assert response.content