fix(server): 修复路由遮蔽、流式背压与鉴权限流顺序

- 专辑搜索静态路由优先注册,避免被 /album/{source}/{id}/{page} 捕获
- mg 流改为异步背压队列,慢客户端不再 QueueFull 悬挂,错误中断响应
- 限流移到鉴权外层,401 爆破计入限流
- 酷我 token 重试移出锁,修复 asyncio.Lock 递归死锁
- 补 CORS、5sing 音质、parse _kind、wy 首次登录 cookie、stream 签名溢出
- mg 流直连、lifespan 关闭 aiohttp 会话、cookie 登录路由与 auth_store 健壮性
- 测试改用 aiohttp + 进程内 uvicorn,移除 httpx/TestClient
- 更新 start.bat 与文档
This commit is contained in:
snowy-yang committed 2026-10-23 23:40:55 +08:00
1 parent 39565a3913
commit 0df18f048b
19 files changed
+534 -241

No files matched your search

+15 -18
View File
@@ -17,8 +17,9 @@ from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Any
from fastapi import APIRouter, FastAPI, Request
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
@@ -28,6 +29,7 @@ from app.http.auth_gate import auth_gate
from app.http.rate_limit import make_rate_limiter
from app.openapi.components import COMPONENT_SCHEMAS
from app.openapi.ui import redoc_html
from app.sdk.common.request import close_session
API_TITLE = "MusicServer 聚合音乐 API"
API_DESCRIPTION = (
@@ -38,21 +40,6 @@ API_DESCRIPTION = (
)
class Envelope(JSONResponse):
"""统一成功响应:{ code: 0, data }。文档里由路由的 responses 引用领域模型。"""
def render(self, content: Any) -> bytes:
return json.dumps({"code": 0, "data": content}, ensure_ascii=False).encode("utf-8")
def ok(data: Any, status_code: int = 200) -> Envelope:
return Envelope(data, status_code=status_code)
def make_router(tag: str) -> APIRouter:
return APIRouter(tags=[tag])
@asynccontextmanager
async def _lifespan(_app: FastAPI) -> AsyncIterator[None]:
"""启动期副作用 —— 登录态自动续期(启动后延时首刷 + 每 24h 一次,与 TS 版一致)。"""
@@ -63,6 +50,7 @@ async def _lifespan(_app: FastAPI) -> AsyncIterator[None]:
yield
finally:
await stop_refresh_scheduler()
await close_session()
def create_app() -> FastAPI:
@@ -89,9 +77,10 @@ def create_app() -> FastAPI:
app.state.started_at = time.time()
app.state.auth_key = config.auth_key()
# ---- 中间行为:限流 → 鉴权(限流先计数,401 爆破同样受限) ----
app.middleware("http")(make_rate_limiter(config.rate_limit()))
# ---- 中间件顺序(后注册者在外层):CORS → no-store → 限流 → 鉴权 ----
# 限流必须在鉴权之外:错误秘钥的 401 也要计数,才能真正防爆破。
app.middleware("http")(auth_gate)
app.middleware("http")(make_rate_limiter(config.rate_limit()))
@app.middleware("http")
async def _no_store(request: Request, call_next):
@@ -99,6 +88,14 @@ def create_app() -> FastAPI:
response.headers["Cache-Control"] = "no-store"
return response
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
max_age=600,
)
# ---- 异常处理 ----
@app.exception_handler(ApiError)
async def _api_error(_: Request, exc: ApiError):