Skip to content

Commit 9edfa8f

Browse files
jirispilkaclaude
andauthored
fix: Show error state in ActorRun widget instead of loading (#571)
* fix: Show error state in ActorRun widget instead of perpetual loading When call-actor returns an isError tool result, the widget now displays a "Failed" badge with a cleaned error message instead of showing "Loading Actor run data ..." forever. Error text is sanitized to strip trailing JSON blobs and model-facing instructions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Update error handling in ActorRun widget to use CallToolResult type --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bddda93 commit 9edfa8f

File tree

3 files changed

+115
-3
lines changed

3 files changed

+115
-3
lines changed

src/web/src/pages/ActorRun/ActorRun.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import { CheckIcon, CrossIcon, LoaderIcon } from "@apify/ui-icons";
66
import { useMcpApp } from "../../context/mcp-app-context";
77
import { useWidgetProps } from "../../hooks/use-widget-props";
88
import { formatDuration, formatTimestamp, humanizeActorName } from "../../utils/formatting";
9+
import { extractActorRunErrorMessage } from "../../utils/actor-run";
910
import { TableSkeleton } from "./ActorRun.skeleton";
11+
1012
interface ActorRunData {
1113
runId: string;
1214
actorName: string;
@@ -305,6 +307,7 @@ export const ActorRun: React.FC = () => {
305307
const toolOutput = useWidgetProps<ToolOutput>();
306308
const toolResponseMetadata = (toolResult?._meta ?? null) as Record<string, unknown> | null;
307309
const stableRunId = getRunIdFromUrl();
310+
const toolErrorMessage = extractActorRunErrorMessage(toolResult);
308311

309312
const [runData, setRunData] = useState<ActorRunData | null>(null);
310313
const [pictureUrl, setPictureUrl] = useState<string | undefined>(undefined);
@@ -464,9 +467,20 @@ export const ActorRun: React.FC = () => {
464467
<WidgetLayout>
465468
<Container>
466469
<EmptyStateContainer>
467-
<Text type="body" size="small" style={{ color: theme.color.neutral.textMuted }}>
468-
Loading Actor run data ...
469-
</Text>
470+
{toolErrorMessage ? (
471+
<>
472+
<Badge variant="danger" size="small" LeadingIcon={CrossIcon}>
473+
Failed
474+
</Badge>
475+
<Text type="body" size="small" style={{ color: theme.color.neutral.text }}>
476+
{toolErrorMessage}
477+
</Text>
478+
</>
479+
) : (
480+
<Text type="body" size="small" style={{ color: theme.color.neutral.textMuted }}>
481+
Loading Actor run data ...
482+
</Text>
483+
)}
470484
</EmptyStateContainer>
471485
</Container>
472486
</WidgetLayout>

src/web/src/utils/actor-run.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2+
3+
/**
4+
* Strip noise that is meant for the model, not the end-user:
5+
* - trailing JSON blobs like `{"statusCode":404}`
6+
* - "Please verify …" / "You can search …" follow-up lines
7+
*/
8+
function cleanErrorText(raw: string): string {
9+
// Take only the first meaningful line (the rest is model guidance)
10+
const firstLine = raw.split("\n")[0].trim();
11+
// Strip trailing JSON blob (e.g. `{"statusCode":404}`)
12+
return firstLine.replace(/\s*\{[^}]*}\s*\.?$/, "").trim();
13+
}
14+
15+
export function extractActorRunErrorMessage(toolResult: CallToolResult | null | undefined): string | null {
16+
if (!toolResult?.isError) {
17+
return null;
18+
}
19+
20+
for (const item of toolResult.content) {
21+
if (typeof item !== "object" || item === null || !("text" in item)) {
22+
continue;
23+
}
24+
25+
const text = item.text;
26+
if (typeof text === "string" && text.trim()) {
27+
const cleaned = cleanErrorText(text);
28+
return cleaned || text.trim();
29+
}
30+
}
31+
32+
return "Actor run failed before it could start.";
33+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { extractActorRunErrorMessage } from '../../src/web/src/utils/actor-run.js';
4+
5+
describe('extractActorRunErrorMessage', () => {
6+
it('strips trailing JSON blob and model instructions from error text', () => {
7+
const message = extractActorRunErrorMessage({
8+
isError: true,
9+
content: [
10+
{
11+
type: 'text',
12+
text: [
13+
'Failed to call Actor \'apify/rag-web-browser\': Actor not found {"statusCode":404}.',
14+
'Please verify the Actor name, input parameters, and ensure the Actor exists.',
15+
'You can search for available Actors using the tool: store-search.',
16+
].join('\n'),
17+
},
18+
],
19+
});
20+
21+
expect(message).toBe("Failed to call Actor 'apify/rag-web-browser': Actor not found");
22+
});
23+
24+
it('returns cleaned first line for single-line errors with JSON blob', () => {
25+
const message = extractActorRunErrorMessage({
26+
isError: true,
27+
content: [
28+
{ type: 'text', text: 'SFAIL Actor not found or definition is not available {"statusCode":404}' },
29+
],
30+
});
31+
32+
expect(message).toBe('SFAIL Actor not found or definition is not available');
33+
});
34+
35+
it('returns null for non-error tool results', () => {
36+
const message = extractActorRunErrorMessage({
37+
isError: false,
38+
content: [
39+
{ type: 'text', text: 'Started Actor "apify/rag-web-browser"' },
40+
],
41+
});
42+
43+
expect(message).toBeNull();
44+
});
45+
46+
it('returns a fallback message when the error response has no text content', () => {
47+
const message = extractActorRunErrorMessage({
48+
isError: true,
49+
content: [{ type: 'image', data: 'abc123', mimeType: 'image/png' }],
50+
});
51+
52+
expect(message).toBe('Actor run failed before it could start.');
53+
});
54+
55+
it('returns plain error text when there is no JSON blob to strip', () => {
56+
const message = extractActorRunErrorMessage({
57+
isError: true,
58+
content: [
59+
{ type: 'text', text: 'Failed to call Actor: connection timeout' },
60+
],
61+
});
62+
63+
expect(message).toBe('Failed to call Actor: connection timeout');
64+
});
65+
});

0 commit comments

Comments
 (0)