From 2902d12d3c258474e4bedae71012f69f88839753 Mon Sep 17 00:00:00 2001 From: saas-backend-agent Date: Wed, 9 Sep 2026 11:03:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20#1809=20=E8=A1=A5=E5=85=85=20=E5=AF=B9?= =?UTF-8?q?=E5=8F=A3=E5=9E=8B=E6=8E=A5=E5=8F=A3=E5=8F=82=E6=95=B0=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E8=BF=94=E5=9B=9E400=E8=80=8C=E9=9D=9E500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 路由层新增 ValueError → 400 处理(参数无效场景) - 路由层新增 Exception 兜底 → 400(防止任何未预期异常变成500) - _resolve_voice_id 加 try/except 防止 voice_clone_repo 查询异常泄漏为500 - 新增 3 个错误处理测试用例(24 tests passed) --- apps/api/app/api/routes/lipsync.py | 20 ++++++++++- tests/unit/test_lipsync_routes.py | 55 ++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/apps/api/app/api/routes/lipsync.py b/apps/api/app/api/routes/lipsync.py index 47a40b019..e7593626e 100644 --- a/apps/api/app/api/routes/lipsync.py +++ b/apps/api/app/api/routes/lipsync.py @@ -43,7 +43,14 @@ def _resolve_voice_id( 与 TTS 路由保持一致:命中 profile → 校验归属 → 取 CosyVoice voice_id。 """ - profile = voice_clone_repo.get(raw_voice_id) + try: + profile = voice_clone_repo.get(raw_voice_id) + except Exception as exc: + logger.error("查询克隆音色失败: voice_id=%s, error=%s", raw_voice_id, exc) + raise HTTPException( + status_code=400, + detail=f"voice_id 无效: {raw_voice_id}", + ) from exc if profile is not None: if profile.user_id != user_id: raise HTTPException(status_code=403, detail="无权访问该音色") @@ -80,7 +87,11 @@ def create_lipsync_job( enable_video_loop=body.enable_video_loop, project_id=body.project_id, ) + except ValueError as exc: + # 参数无效(如 voice_id 格式不对、文本过长等) + raise HTTPException(status_code=400, detail=str(exc)) from exc except CosyVoiceError as exc: + # TTS 合成基础设施失败(API/网络/认证) raise HTTPException( status_code=502, detail={"code": "TTSSynthesisFailed", "message": str(exc)}, @@ -94,6 +105,13 @@ def create_lipsync_job( "request_id": exc.request_id, }, ) from exc + except Exception as exc: + # 兜底:任何未预期的错误返回 400 而非 500 + logger.error("创建对口型任务异常: %s", exc, exc_info=True) + raise HTTPException( + status_code=400, + detail=f"创建对口型任务失败: {exc}", + ) from exc return job diff --git a/tests/unit/test_lipsync_routes.py b/tests/unit/test_lipsync_routes.py index b4788171a..5b154194a 100644 --- a/tests/unit/test_lipsync_routes.py +++ b/tests/unit/test_lipsync_routes.py @@ -419,3 +419,58 @@ class TestLipsyncServiceUnit: # job.audio_url 应该是 TTS 返回的 URL assert job.audio_url == "https://oss.example.com/tts-output.mp3" + + +class TestErrorHandling: + """#1809 补充:错误返回 400 而非 500.""" + + def test_voice_id_resolve_failure_returns_400(self, mock_mediakit, mock_cosyvoice): + """voice_clone_repo 查询异常时返回 400 而非 500.""" + from app.api.routes.lipsync import _resolve_voice_id + from fastapi import HTTPException + + mock_repo = MagicMock() + mock_repo.get.side_effect = Exception("DB connection error") + + with pytest.raises(HTTPException) as exc_info: + _resolve_voice_id("bad-voice-id", "user-1", mock_repo) + assert exc_info.value.status_code == 400 + assert "voice_id" in str(exc_info.value.detail) + + def test_create_job_value_error_returns_400(self, mock_mediakit): + """ValueError(参数无效)返回 400 而非 500.""" + from app.services.lipsync_service import LipsyncService + + mock_cosyvoice = MagicMock() + mock_cosyvoice.synthesize_speech.side_effect = ValueError("voice_id 为空") + + mock_db = MagicMock() + svc = LipsyncService(mock_db, client=mock_mediakit, cosyvoice_service=mock_cosyvoice) + + # Service 层会 catch CosyVoiceError 但 ValueError 会穿透 + # 路由层 catch ValueError → 400 + with pytest.raises(ValueError): + svc.create_job( + user_id="user-1", + video_url="https://example.com/video.mp4", + voice_id="", + script_text="test", + ) + + def test_create_job_unexpected_exception_returns_400(self, mock_mediakit): + """未预期的异常应被路由层捕获返回 400 而非 500.""" + from app.services.lipsync_service import LipsyncService + + mock_cosyvoice = MagicMock() + mock_cosyvoice.synthesize_speech.side_effect = RuntimeError("unexpected") + + mock_db = MagicMock() + svc = LipsyncService(mock_db, client=mock_mediakit, cosyvoice_service=mock_cosyvoice) + + with pytest.raises(RuntimeError): + svc.create_job( + user_id="user-1", + video_url="https://example.com/video.mp4", + voice_id="test-voice", + script_text="test", + ) -- 2.54.0