mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-12 14:00:35 +00:00
fix(frontend): make chat stream server errors actionable (#8041)
* fix(frontend): make chat stream server errors actionable * fix(frontend): log chat stream server errors --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
fa8e015023
commit
2598940283
@ -59,7 +59,7 @@ function injectCsrfHeader(_url: URL, init: RequestInit): RequestInit {
|
||||
//
|
||||
// ``error``/``timeout`` are terminal too, so a reload within the ~60s
|
||||
// bridge-reap window no longer replays the buffered error event through
|
||||
// ``onError`` — the transient error toast (``getStreamErrorMessage``) is
|
||||
// ``onError`` — the transient error toast (``getStreamErrorToastMessage``) is
|
||||
// dropped. The persisted error state still loads from the checkpoint via
|
||||
// ``useThreadHistory``, so only the toast is lost; that is intentional, since
|
||||
// surfacing a stale error toast on every reload is noise rather than signal.
|
||||
|
||||
@ -1637,6 +1637,8 @@ export const enUS: Translations = {
|
||||
branchFailed: "Failed to branch conversation.",
|
||||
streamReplayGap:
|
||||
"Some live updates expired. The conversation was restored from saved state.",
|
||||
streamServerError: (status) =>
|
||||
`The service is temporarily unavailable (HTTP ${status}). Please try again.`,
|
||||
outlineLabel: "Conversation outline",
|
||||
outlineAttachmentFallback: "Image or file message",
|
||||
},
|
||||
|
||||
@ -1329,6 +1329,7 @@ export interface Translations {
|
||||
branchCreated: string;
|
||||
branchFailed: string;
|
||||
streamReplayGap: string;
|
||||
streamServerError: (status: number) => string;
|
||||
outlineLabel: string;
|
||||
outlineAttachmentFallback: string;
|
||||
};
|
||||
|
||||
@ -1538,6 +1538,8 @@ export const zhCN: Translations = {
|
||||
branchCreated: "已创建分叉对话",
|
||||
branchFailed: "创建分叉对话失败。",
|
||||
streamReplayGap: "部分实时更新已过期,已从持久化状态恢复对话。",
|
||||
streamServerError: (status) =>
|
||||
`服务暂时不可用(HTTP ${status}),请稍后重试。`,
|
||||
outlineLabel: "对话章节",
|
||||
outlineAttachmentFallback: "图片或文件消息",
|
||||
},
|
||||
|
||||
@ -30,6 +30,7 @@ import { getBackendBaseURL } from "../config";
|
||||
import { useRunInputsEnabled } from "../features";
|
||||
import { useI18n } from "../i18n/hooks";
|
||||
import type { Locale } from "../i18n/locale";
|
||||
import type { Translations } from "../i18n/locales";
|
||||
import {
|
||||
extractHitlApprovalRequests,
|
||||
type HitlApprovalRequest,
|
||||
@ -2054,6 +2055,30 @@ function getHttpStatus(error: unknown): number | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getStreamErrorToastMessage(
|
||||
error: unknown,
|
||||
t: Translations,
|
||||
showApprovalErrors = false,
|
||||
): string {
|
||||
const status = getHttpStatus(error);
|
||||
if (status !== undefined && status >= 500 && status < 600) {
|
||||
console.warn("Chat stream request failed:", getStreamErrorMessage(error));
|
||||
return t.conversation.streamServerError(status);
|
||||
}
|
||||
|
||||
const message = getStreamErrorMessage(error);
|
||||
if (showApprovalErrors) {
|
||||
const errorText = `${JSON.stringify(error) ?? ""} ${message}`;
|
||||
if (errorText.includes("hitl_approval_disabled")) {
|
||||
return t.humanInput.approval.featureDisabled;
|
||||
}
|
||||
if (errorText.includes("no_pending_approval")) {
|
||||
return t.humanInput.approval.noPending;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
function isThreadMissingError(error: unknown): boolean {
|
||||
const status = getHttpStatus(error);
|
||||
// Treat 403 like 404 here to avoid disclosing whether an inaccessible thread
|
||||
@ -2528,16 +2553,7 @@ export function useThreadStream({
|
||||
pendingPreparedReplayRef.current = null;
|
||||
setPendingSupersededRunIds(new Set());
|
||||
setPendingSupersededMessageIds(new Set());
|
||||
const streamErrorText = `${JSON.stringify(error) ?? ""} ${getStreamErrorMessage(
|
||||
error,
|
||||
)}`;
|
||||
toast.error(
|
||||
streamErrorText.includes("hitl_approval_disabled")
|
||||
? t.humanInput.approval.featureDisabled
|
||||
: streamErrorText.includes("no_pending_approval")
|
||||
? t.humanInput.approval.noPending
|
||||
: getStreamErrorMessage(error),
|
||||
);
|
||||
toast.error(getStreamErrorToastMessage(error, t, true));
|
||||
pendingUsageBaselineMessageIdsRef.current = new Set(
|
||||
messagesRef.current
|
||||
.map(messageIdentity)
|
||||
@ -3281,7 +3297,7 @@ export function useThreadStream({
|
||||
};
|
||||
toast.error(t.humanInput.approval.submitTimeout);
|
||||
void stopThread().catch((error: unknown) => {
|
||||
toast.error(getStreamErrorMessage(error));
|
||||
toast.error(getStreamErrorToastMessage(error, t));
|
||||
});
|
||||
}
|
||||
}, HITL_APPROVAL_ACCEPTANCE_TIMEOUT_MS);
|
||||
@ -3326,16 +3342,7 @@ export function useThreadStream({
|
||||
if (pendingApprovalSubmissionRef.current === submission) {
|
||||
settlePendingApproval(false);
|
||||
}
|
||||
const serialized = `${JSON.stringify(error) ?? ""} ${getStreamErrorMessage(
|
||||
error,
|
||||
)}`;
|
||||
toast.error(
|
||||
serialized.includes("hitl_approval_disabled")
|
||||
? t.humanInput.approval.featureDisabled
|
||||
: serialized.includes("no_pending_approval")
|
||||
? t.humanInput.approval.noPending
|
||||
: getStreamErrorMessage(error),
|
||||
);
|
||||
toast.error(getStreamErrorToastMessage(error, t, true));
|
||||
return false;
|
||||
} finally {
|
||||
sendInFlightRef.current = false;
|
||||
@ -3482,7 +3489,7 @@ export function useThreadStream({
|
||||
removeSetItems(current, preparedSupersededMessageIds),
|
||||
);
|
||||
}
|
||||
toast.error(getStreamErrorMessage(error));
|
||||
toast.error(getStreamErrorToastMessage(error, t));
|
||||
return false;
|
||||
} finally {
|
||||
sendInFlightRef.current = false;
|
||||
@ -3494,6 +3501,7 @@ export function useThreadStream({
|
||||
humanMessageCount,
|
||||
persistedMessages,
|
||||
queryClient,
|
||||
t,
|
||||
thread,
|
||||
visibleHistory,
|
||||
],
|
||||
|
||||
50
frontend/tests/e2e/chat-stream-error.spec.ts
Normal file
50
frontend/tests/e2e/chat-stream-error.spec.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { mockLangGraphAPI } from "./utils/mock-api";
|
||||
|
||||
for (const { status, body, message } of [
|
||||
{
|
||||
status: 502,
|
||||
body: '{"detail":"upstream failed"}',
|
||||
message:
|
||||
"The service is temporarily unavailable (HTTP 502). Please try again.",
|
||||
},
|
||||
{
|
||||
status: 409,
|
||||
body: '{"detail":"Another run is active"}',
|
||||
message: 'HTTP 409: {"detail":"Another run is active"}',
|
||||
},
|
||||
]) {
|
||||
test(`shows the expected chat error for HTTP ${status}`, async ({ page }) => {
|
||||
const streamWarnings: string[] = [];
|
||||
page.on("console", (entry) => {
|
||||
if (
|
||||
entry.type() === "warning" &&
|
||||
entry.text().startsWith("Chat stream request failed:")
|
||||
) {
|
||||
streamWarnings.push(entry.text());
|
||||
}
|
||||
});
|
||||
|
||||
mockLangGraphAPI(page);
|
||||
await page.route(
|
||||
/\/api\/langgraph\/(?:threads\/[^/]+\/)?runs\/stream$/,
|
||||
(route) =>
|
||||
route.fulfill({ status, contentType: "application/json", body }),
|
||||
);
|
||||
|
||||
await page.goto("/workspace/chats/new");
|
||||
const composer = page.getByRole("textbox", {
|
||||
name: /how can i assist you/i,
|
||||
});
|
||||
await composer.fill("Send a message");
|
||||
await composer.press("Enter");
|
||||
|
||||
await expect(page.locator("[data-sonner-toast]").last()).toHaveText(
|
||||
message,
|
||||
);
|
||||
expect(streamWarnings).toEqual(
|
||||
status === 502 ? [`Chat stream request failed: HTTP 502: ${body}`] : [],
|
||||
);
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user