fix: eagerly validate conversation on service-api and explore chat endpoints (#38801)

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Yufeng He 2026-07-12 23:19:07 +08:00 committed by GitHub
parent b54107e9b9
commit fde08d24fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 83 additions and 0 deletions

View File

@ -36,6 +36,7 @@ from models import Account
from models.model import AppMode, InstalledApp
from services.app_generate_service import AppGenerateService
from services.app_task_service import AppTaskService
from services.conversation_service import ConversationService
from services.errors.llm import InvokeRateLimitError
from .. import console_ns
@ -188,6 +189,15 @@ class ChatApi(InstalledAppResource):
db.session.commit()
try:
# Eagerly validate conversation to avoid hanging on invalid conversation_id
if payload.conversation_id:
ConversationService.get_conversation(
app_model=app_model,
conversation_id=payload.conversation_id,
user=current_user,
session=db.session(),
)
response = AppGenerateService.generate(
session=session,
app_model=app_model,

View File

@ -40,12 +40,14 @@ from core.errors.error import (
QuotaExceededError,
)
from core.helper.trace_id_helper import get_external_trace_id, get_trace_session_id, omit_trace_session_id_from_payload
from extensions.ext_database import db
from graphon.model_runtime.errors.invoke import InvokeError
from libs import helper
from libs.helper import UUIDStrOrEmpty
from models.model import App, AppMode, EndUser
from services.app_generate_service import AppGenerateService
from services.app_task_service import AppTaskService
from services.conversation_service import ConversationService
from services.errors.app import IsDraftWorkflowError, WorkflowIdFormatError, WorkflowNotFoundError
from services.errors.llm import InvokeRateLimitError
@ -377,6 +379,15 @@ class ChatApi(Resource):
streaming = _resolve_agent_app_streaming(app_mode=app_mode, response_mode=payload.response_mode)
try:
# Eagerly validate conversation to avoid hanging on invalid conversation_id
if payload.conversation_id:
ConversationService.get_conversation(
app_model=app_model,
conversation_id=payload.conversation_id,
user=end_user,
session=db.session(),
)
response = AppGenerateService.generate(
session=session,
app_model=app_model,

View File

@ -1,3 +1,4 @@
import uuid
from inspect import unwrap
from unittest.mock import MagicMock, PropertyMock, patch
@ -310,6 +311,37 @@ class TestChatApi:
with pytest.raises(completion_module.NotFound):
method(api, MagicMock(), user, chat_app)
def test_invalid_conversation_id_fails_fast_as_not_found(self, app: Flask, chat_app, user) -> None:
# A nonexistent conversation_id must fail fast as 404, before the streaming
# generator is created. Previously the lookup only ran inside the generator,
# so an invalid id surfaced as a hang instead of a clean error.
payload_patch = patch.object(
type(completion_module.console_ns),
"payload",
new_callable=PropertyMock,
return_value={"inputs": {}, "query": "hi", "conversation_id": str(uuid.uuid4())},
)
generate_mock = MagicMock(return_value={"ok": True})
api = completion_module.ChatApi()
method = unwrap(api.post)
with (
app.test_request_context("/", json={}),
payload_patch,
patch.object(
completion_module.ConversationService,
"get_conversation",
side_effect=completion_module.services.errors.conversation.ConversationNotExistsError(),
),
patch.object(completion_module.AppGenerateService, "generate", generate_mock),
):
with pytest.raises(completion_module.NotFound):
method(api, MagicMock(), user, chat_app)
# The lookup must run before generation, so the generator is never started.
generate_mock.assert_not_called()
def test_app_unavailable_chat(self, app: Flask, chat_app, user, payload_patch):
api = completion_module.ChatApi()
method = unwrap(api.post)

View File

@ -42,6 +42,7 @@ from graphon.model_runtime.errors.invoke import InvokeError
from models.model import App, AppMode, EndUser
from services.app_generate_service import AppGenerateService
from services.app_task_service import AppTaskService
from services.conversation_service import ConversationService
from services.errors.app import IsDraftWorkflowError, WorkflowIdFormatError, WorkflowNotFoundError
from services.errors.conversation import ConversationNotExistsError
from services.errors.llm import InvokeRateLimitError
@ -554,6 +555,35 @@ class TestChatApiController:
with pytest.raises(AgentNotPublishedError):
handler(api, session=Mock(), app_model=app_model, end_user=end_user)
def test_invalid_conversation_id_fails_fast_as_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
# A well-formed but nonexistent conversation_id must fail fast as 404, before the
# streaming generator is created. Previously the lookup only ran inside the generator,
# so an invalid id surfaced as a hang instead of a clean error.
monkeypatch.setattr(
ConversationService,
"get_conversation",
lambda *_args, **_kwargs: (_ for _ in ()).throw(ConversationNotExistsError()),
)
generate_mock = Mock(return_value={"text": "unused"})
monkeypatch.setattr(AppGenerateService, "generate", generate_mock)
api = ChatApi()
handler = unwrap(api.post)
app_model = SimpleNamespace(mode=AppMode.CHAT.value, id="app-1")
end_user = SimpleNamespace()
with app.test_request_context(
"/chat-messages",
method="POST",
json={"inputs": {}, "query": "hi", "conversation_id": str(uuid.uuid4())},
):
with pytest.raises(NotFound):
handler(api, session=Mock(), app_model=app_model, end_user=end_user)
# The lookup must run before generation, so the generator is never started.
generate_mock.assert_not_called()
class TestChatStopApiController:
def test_wrong_mode(self, app: Flask) -> None: