Skip to content

Commit ae5de08

Browse files
committed
fix(pearl): correct SSE error event format and fix infinite loading on missing API key
Three related bugs fixed: 1. Wrong SSE error event shape in backend routes - bubble-flows.ts and ai.ts were sending { type: 'error', error: '...' } - StreamingEvent schema requires { type: 'error', data: { error, recoverable } } - Frontend handler reads event.data.error, so errors silently fell back to 'An error occurred' or were swallowed entirely 2. Missing error event on coffee agent failure (planning phase) - When GOOGLE_API_KEY is absent, coffee.ts returns early with success:false - The route only sent stream_complete with the error buried in coffeeResult - No error SSE event was emitted, so the frontend never received it 3. UI stuck in infinite loading state when API key is missing - generationAbortController was never cleared after the stream ended - isGenerating stayed true forever since setGenerationCompleted was never called - Added stream_complete error handling in handleStreamingEvent as a fallback - Added safety net in startGenerationStream to always clear isGenerating after the stream loop exits without a generation_complete event
1 parent 06974f8 commit ae5de08

File tree

4 files changed

+61
-15
lines changed

4 files changed

+61
-15
lines changed

apps/bubble-studio/src/hooks/usePearlChatStore.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,9 +424,25 @@ export function handleStreamingEvent(
424424
case 'complete':
425425
case 'coffee_complete':
426426
case 'heartbeat':
427-
case 'stream_complete':
428-
// Ignore these events - they're control signals, not display events
429427
break;
428+
429+
case 'stream_complete': {
430+
// Check if the backend returned an error result (e.g. missing API key)
431+
const streamData = event as unknown as {
432+
coffeeResult?: { type?: string; error?: string; success?: boolean };
433+
};
434+
if (
435+
streamData.coffeeResult &&
436+
streamData.coffeeResult.success === false
437+
) {
438+
const errorMsg =
439+
streamData.coffeeResult.error ||
440+
'Generation failed. Please check your API key configuration.';
441+
state.addEvent({ type: 'generation_error', message: errorMsg });
442+
state.setGenerationCompleted(true); // also clears abort controller + isGenerating
443+
}
444+
break;
445+
}
430446
}
431447
}
432448

apps/bubble-studio/src/hooks/usePearlStream.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ async function startGenerationStream(
221221
`[startGenerationStream] Stream completed successfully for flow ${flowId}`
222222
);
223223
pearlStore.getState().setIsCoffeeLoading(false);
224+
// Safety net: if no generation_complete/error event was received, clear the
225+
// generating state so the UI doesn't stay stuck in a loading loop.
226+
if (!pearlStore.getState().generationCompleted) {
227+
pearlStore.getState().cancelGenerationStream();
228+
}
224229
return;
225230
} catch (error) {
226231
if (abortController.signal.aborted) {

apps/bubblelab-api/src/routes/ai.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,13 @@ app.openapi(pearlRoute, async (c) => {
148148
await stream.writeSSE({
149149
data: JSON.stringify({
150150
type: 'error',
151-
error:
152-
error instanceof Error ? error.message : 'Unknown streaming error',
153-
recoverable: false,
151+
data: {
152+
error:
153+
error instanceof Error
154+
? error.message
155+
: 'Unknown streaming error',
156+
recoverable: false,
157+
},
154158
}),
155159
event: 'error',
156160
});

apps/bubblelab-api/src/routes/bubble-flows.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -508,11 +508,13 @@ app.openapi(executeBubbleFlowStreamRoute, async (c) => {
508508
await stream.writeSSE({
509509
data: JSON.stringify({
510510
type: 'error',
511-
error:
512-
error instanceof Error
513-
? error.message
514-
: 'Unknown streaming error',
515-
recoverable: false,
511+
data: {
512+
error:
513+
error instanceof Error
514+
? error.message
515+
: 'Unknown streaming error',
516+
recoverable: false,
517+
},
516518
}),
517519
event: 'error',
518520
});
@@ -1416,6 +1418,23 @@ app.openapi(generateBubbleFlowCodeRoute, async (c) => {
14161418
// Clear heartbeat and send stream completion
14171419
clearInterval(heartbeatInterval);
14181420

1421+
// Emit a proper error event when coffee fails (e.g. missing API key)
1422+
// before stream_complete so the frontend error handler picks it up.
1423+
if (!coffeeResult.success) {
1424+
await stream.writeSSE({
1425+
data: JSON.stringify({
1426+
type: 'error',
1427+
data: {
1428+
error:
1429+
coffeeResult.error ||
1430+
'Flow generation failed. Please check your API key configuration.',
1431+
recoverable: false,
1432+
},
1433+
}),
1434+
event: 'error',
1435+
});
1436+
}
1437+
14191438
await stream.writeSSE({
14201439
data: JSON.stringify({
14211440
type: 'stream_complete',
@@ -1690,11 +1709,13 @@ app.openapi(generateBubbleFlowCodeRoute, async (c) => {
16901709
await stream.writeSSE({
16911710
data: JSON.stringify({
16921711
type: 'error',
1693-
error:
1694-
error instanceof Error
1695-
? error.message
1696-
: 'Unknown streaming error',
1697-
recoverable: false,
1712+
data: {
1713+
error:
1714+
error instanceof Error
1715+
? error.message
1716+
: 'Unknown streaming error',
1717+
recoverable: false,
1718+
},
16981719
}),
16991720
event: 'error',
17001721
});

0 commit comments

Comments
 (0)