Files
Random-image-api/backend/app/core/http_stream.py
T
2026-02-22 18:58:55 +08:00

99 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from typing import Any
import httpx
from starlette.responses import StreamingResponse
from app.core.errors import ApiError, ErrorCode
from app.core.metrics import UPSTREAM_STREAM_ERRORS_TOTAL
PIXIV_REFERER = "https://www.pixiv.net/"
async def stream_url(
url: str,
*,
transport: httpx.BaseTransport | None = None,
proxy: str | None = None,
cache_control: str,
referer: str = PIXIV_REFERER,
timeout_s: float = 30.0,
range_header: str | None = None,
) -> StreamingResponse:
client = httpx.AsyncClient(
transport=transport,
proxy=proxy,
follow_redirects=True,
timeout=httpx.Timeout(timeout_s, connect=10.0),
)
request_headers: dict[str, str] = {}
if referer:
request_headers["Referer"] = referer
if range_header:
request_headers["Range"] = range_header
request = client.build_request("GET", url, headers=request_headers)
try:
upstream = await client.send(request, stream=True)
except httpx.ProxyError as exc:
UPSTREAM_STREAM_ERRORS_TOTAL.inc()
await client.aclose()
msg = str(exc).lower()
if "407" in msg or "proxy authentication" in msg:
raise ApiError(
code=ErrorCode.PROXY_AUTH_FAILED,
message="代理认证失败",
status_code=502,
) from exc
raise ApiError(
code=ErrorCode.PROXY_CONNECT_FAILED,
message="代理连接失败",
status_code=502,
) from exc
except Exception as exc:
UPSTREAM_STREAM_ERRORS_TOTAL.inc()
await client.aclose()
raise ApiError(code=ErrorCode.UPSTREAM_STREAM_ERROR, message="上游请求失败", status_code=502) from exc
if upstream.status_code not in {200, 206}:
status = upstream.status_code
UPSTREAM_STREAM_ERRORS_TOTAL.inc()
await upstream.aclose()
await client.aclose()
if status == 403:
raise ApiError(code=ErrorCode.UPSTREAM_403, message="上游拒绝访问(403)", status_code=502)
if status == 404:
raise ApiError(code=ErrorCode.UPSTREAM_404, message="上游资源不存在(404)", status_code=502)
if status == 429:
raise ApiError(code=ErrorCode.UPSTREAM_RATE_LIMIT, message="上游触发限流(429)", status_code=502)
raise ApiError(code=ErrorCode.UPSTREAM_STREAM_ERROR, message="上游错误", status_code=502)
media_type = upstream.headers.get("content-type") or "application/octet-stream"
content_length = upstream.headers.get("content-length")
async def _iter_bytes() -> Any:
try:
async for chunk in upstream.aiter_bytes():
yield chunk
except Exception:
UPSTREAM_STREAM_ERRORS_TOTAL.inc()
raise
finally:
await upstream.aclose()
await client.aclose()
accept_ranges = upstream.headers.get("accept-ranges")
content_range = upstream.headers.get("content-range")
resp = StreamingResponse(_iter_bytes(), status_code=upstream.status_code, media_type=media_type)
resp.headers["Cache-Control"] = cache_control
if content_length:
resp.headers["Content-Length"] = content_length
if accept_ranges:
resp.headers["Accept-Ranges"] = accept_ranges
if content_range:
resp.headers["Content-Range"] = content_range
return resp