fix(download): preserve protocol failures and isolate site updates (#7143)

* fix(download): preserve protocol failures and isolate site updates

* test: cover tracker token lifecycle and record dependency facts

---------

Co-authored-by: jxxghp <jxxghp@gmail.com>
This commit is contained in:
Aqr-K
2026-10-23 07:22:22 +08:00
committed by GitHub
parent d666ae9d86
commit 598d1663b1
33 changed files with 1553 additions and 254 deletions

View File

@@ -7,6 +7,7 @@ from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Optional, Union, cast
from urllib.parse import urljoin, urlsplit
from app.adapters.network.doh import DohHelper
from app.adapters.network.http import AsyncRequestUtils, RequestUtils
@@ -59,6 +60,7 @@ from app.chain.system import (
configure_system_ports,
reset_system_ports,
)
from app.foundation.url import UrlUtils
from app.runtime.log import logger
@@ -406,12 +408,57 @@ class _DownloadHttpAdapter:
"""
发送下载链 GET 请求并原样保留响应三态。
"""
request = self._request(cookies=cookies, ua=ua, headers=headers, proxies=proxies, timeout=timeout)
kwargs: dict[str, Any] = {"raise_exception": raise_exception}
if params is not None:
kwargs["params"] = params
response = request.get_res(url, **kwargs)
return cast(Optional[DownloadResponsePort], response)
try:
return self._get_redirects(url, cookies=cookies, ua=ua, headers=headers, proxies=proxies,
timeout=timeout, params=params, raise_exception=raise_exception)
except ValueError:
if raise_exception:
raise
return None
def _get_redirects(
self, url: str, *, cookies: Optional[Union[str, dict[str, str]]], ua: Optional[str],
headers: Optional[dict[str, str]], proxies: Optional[dict[str, str]], timeout: Optional[int],
params: Optional[dict[str, Any]], raise_exception: bool,
) -> Optional[DownloadResponsePort]:
"""
跟随有界GET跳转;跨来源后不恢复Cookie、Authorization或Referer。
:param url: 当前下载地址
:param cookies: 仅供原始来源使用的Cookie
:param ua: 请求用户代理
:param headers: 原始来源额外请求头
:param proxies: 本次请求代理
:param timeout: 请求超时
:param params: 仅首跳附加的查询参数
:param raise_exception: 是否保留底层传输异常
:return: 最终响应或无响应
:raises ValueError: 跳转非法、重复或超过上限
"""
seen, request_headers = set(), dict(headers or {})
for _hop in range(9):
if url in seen:
raise ValueError("下载重定向出现环路")
seen.add(url)
request = self._request(cookies=cookies, ua=ua, headers=request_headers, proxies=proxies, timeout=timeout)
kwargs: dict[str, Any] = {"raise_exception": raise_exception, "allow_redirects": False}
if params is not None:
kwargs["params"] = params
response = cast(Optional[DownloadResponsePort], request.get_res(url, **kwargs))
if response is None or response.status_code not in (301, 302, 303, 307, 308):
return response
location = response.headers.get("Location") or response.headers.get("location")
response.close()
destination = urljoin(url, location or "")
target = urlsplit(destination)
if not location or UrlUtils.normalize_http_url(destination) is None or target.username or target.password:
raise ValueError("下载重定向目标无效")
if not UrlUtils.same_origin(url, destination):
cookies = None
request_headers = {key: value for key, value in request_headers.items()
if key.lower() not in ("cookie", "authorization", "proxy-authorization", "referer")}
url, params = destination, None
raise ValueError("下载重定向次数超过上限")
def post(
self,

View File

@@ -4,6 +4,7 @@
from contextlib import contextmanager
from typing import Any, Callable, Iterator, Mapping, Optional, cast
from urllib.parse import urljoin, urlsplit
from app.adapters.external.cookiecloud import CookieCloudHelper
from app.adapters.external.ocr import OcrHelper
@@ -42,6 +43,7 @@ from app.chain.site import (
configure_site_ports,
reset_site_ports,
)
from app.foundation.url import UrlUtils
class _RssHttpAdapter:
@@ -222,16 +224,38 @@ class _SiteHttpAdapter:
:param json_body: JSON 请求体,None 时保留无请求体行为
:yields: 在上下文有效期内提供响应或 None
"""
request = RequestUtils(
headers=dict(headers or {}),
cookies=cookie or "",
ua=ua or "",
proxies=dict(proxies or {}),
timeout=timeout,
)
request_options = {} if json_body is None else {"json": dict(json_body)}
with request.response_manager(method=method, url=url, **request_options) as response:
yield cast(Optional[SiteResponsePort], response)
request_headers = dict(headers or {})
seen = set()
for _hop in range(9):
if url in seen:
raise ValueError("站点请求重定向出现环路")
seen.add(url)
request = RequestUtils(
headers=request_headers, cookies=cookie or "", ua=ua or "",
proxies=dict(proxies or {}), timeout=timeout,
)
request_options: dict[str, Any] = {} if json_body is None else {"json": dict(json_body)}
if method.upper() == "GET":
request_options["allow_redirects"] = False
with request.response_manager(method=method, url=url, **request_options) as response:
if method.upper() != "GET" or response is None or response.status_code not in (301, 302, 303, 307, 308):
yield cast(Optional[SiteResponsePort], response)
return
location = response.headers.get("Location")
if not location:
raise ValueError("站点请求重定向缺少目标地址")
destination = urljoin(url, location)
target = urlsplit(destination)
if target.scheme not in ("http", "https") or not target.hostname or target.username or target.password:
raise ValueError("站点请求重定向目标无效")
if not UrlUtils.same_origin(url, destination):
cookie = None
request_headers = {
key: value for key, value in request_headers.items()
if key.lower() not in ("cookie", "authorization", "proxy-authorization", "referer")
}
url = destination
raise ValueError("站点请求重定向次数超过上限")
class _SiteBrowserAdapter: