Skip to content

Commit 8130789

Browse files
committed
fix(network): follow 302 redirects in shared httpx client (#983)
Mikanime's mirror (and some CDN-fronted sources) respond with 302 to the canonical host. httpx AsyncClient defaults to follow_redirects=False, so raise_for_status surfaced the 302 as an HTTPStatusError and the RSS pull failed in a retry loop. Enable follow_redirects=True for every construction of the shared client (proxy, socks5, and direct branches) via a shared kwargs dict. Closes #983
1 parent 4e9d8b3 commit 8130789

2 files changed

Lines changed: 30 additions & 16 deletions

File tree

backend/src/module/network/request_url.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,30 +36,32 @@ async def get_shared_client() -> httpx.AsyncClient:
3636
if _shared_client is not None:
3737
await _shared_client.aclose()
3838
timeout = httpx.Timeout(connect=10.0, read=30.0, write=10.0, pool=10.0)
39+
# follow_redirects=True: Mikan mirrors and some CDNs respond with 302 to the
40+
# canonical host; without this, raise_for_status treats the redirect as an
41+
# error and the RSS pull fails (#983).
42+
common_kwargs = {
43+
"timeout": timeout,
44+
"limits": _CONNECTION_LIMITS,
45+
"follow_redirects": True,
46+
}
3947
if settings.proxy.enable:
4048
if "http" in settings.proxy.type:
4149
if settings.proxy.username:
4250
proxy_url = f"http://{settings.proxy.username}:{settings.proxy.password}@{settings.proxy.host}:{settings.proxy.port}"
4351
else:
4452
proxy_url = f"http://{settings.proxy.host}:{settings.proxy.port}"
45-
_shared_client = httpx.AsyncClient(
46-
proxy=proxy_url, timeout=timeout, limits=_CONNECTION_LIMITS
47-
)
53+
_shared_client = httpx.AsyncClient(proxy=proxy_url, **common_kwargs)
4854
elif settings.proxy.type == "socks5":
4955
if settings.proxy.username:
5056
socks_url = f"socks5://{settings.proxy.username}:{settings.proxy.password}@{settings.proxy.host}:{settings.proxy.port}"
5157
else:
5258
socks_url = f"socks5://{settings.proxy.host}:{settings.proxy.port}"
5359
transport = AsyncProxyTransport.from_url(socks_url, rdns=True)
54-
_shared_client = httpx.AsyncClient(
55-
transport=transport, timeout=timeout, limits=_CONNECTION_LIMITS
56-
)
60+
_shared_client = httpx.AsyncClient(transport=transport, **common_kwargs)
5761
else:
58-
_shared_client = httpx.AsyncClient(
59-
timeout=timeout, limits=_CONNECTION_LIMITS
60-
)
62+
_shared_client = httpx.AsyncClient(**common_kwargs)
6163
else:
62-
_shared_client = httpx.AsyncClient(timeout=timeout, limits=_CONNECTION_LIMITS)
64+
_shared_client = httpx.AsyncClient(**common_kwargs)
6365
_shared_client_proxy_key = current_key
6466
return _shared_client
6567

@@ -91,7 +93,9 @@ def _get_headers(self, url: str) -> dict:
9193
}
9294
# For torrent files, use different Accept header
9395
if url.endswith(".torrent") or "/download/" in url:
94-
base_headers["Accept"] = "application/x-bittorrent, application/octet-stream, */*"
96+
base_headers["Accept"] = (
97+
"application/x-bittorrent, application/octet-stream, */*"
98+
)
9599
else:
96100
base_headers["Accept"] = "application/xml, text/xml, */*"
97101
return base_headers
@@ -102,7 +106,11 @@ async def get_url(self, url, retry=3):
102106
while True:
103107
try:
104108
req = await self._client.get(url=url, headers=headers)
105-
logger.debug("[Network] Successfully connected to %s. Status: %s", url, req.status_code)
109+
logger.debug(
110+
"[Network] Successfully connected to %s. Status: %s",
111+
url,
112+
req.status_code,
113+
)
106114
req.raise_for_status()
107115
return req
108116
except httpx.HTTPStatusError as e:
@@ -122,16 +130,16 @@ async def get_url(self, url, retry=3):
122130
except Exception as e:
123131
logger.warning(f"[Network] Unexpected error for {url}: {e}")
124132
break
125-
logger.error(f"[Network] Unable to connect to {url}, Please check your network settings")
133+
logger.error(
134+
f"[Network] Unable to connect to {url}, Please check your network settings"
135+
)
126136
return None
127137

128138
async def post_url(self, url: str, data: dict, retry=3):
129139
try_time = 0
130140
while True:
131141
try:
132-
req = await self._client.post(
133-
url=url, headers=self.header, data=data
134-
)
142+
req = await self._client.post(url=url, headers=self.header, data=data)
135143
req.raise_for_status()
136144
return req
137145
except httpx.RequestError:

backend/src/test/test_request_url.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ async def test_client_has_max_connections(self):
3333
assert pool._max_connections is not None
3434
assert pool._max_connections > 0
3535

36+
async def test_client_follows_redirects(self):
37+
"""Regression for #983: mikanime mirror returns 302 to the canonical
38+
URL but httpx refuses to follow by default, so the RSS fetch fails."""
39+
client = await get_shared_client()
40+
assert client.follow_redirects is True
41+
3642

3743
class TestResetSharedClient:
3844
async def test_reset_closes_existing_client(self):

0 commit comments

Comments
 (0)