From 82f1e99324b1f4e29908a63297031bc8e480c8b6 Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 4 Mar 2026 22:11:18 +0000 Subject: [PATCH] fix(app): wrap unprotected JSON.parse calls in try/catch SSE stream callbacks and AI response parsing now handle malformed data gracefully instead of throwing unhandled exceptions. Co-Authored-By: Claude Opus 4.6 --- packages/app/src/adapters/claude-adapter.ts | 14 +++++++----- .../app/src/adapters/openrouter-adapter.ts | 8 ++++--- packages/app/src/composables/useAI.ts | 22 +++++++++++-------- .../app/src/composables/useSimilarContent.ts | 5 ++++- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/packages/app/src/adapters/claude-adapter.ts b/packages/app/src/adapters/claude-adapter.ts index 3cf7f104..1834eddf 100644 --- a/packages/app/src/adapters/claude-adapter.ts +++ b/packages/app/src/adapters/claude-adapter.ts @@ -51,12 +51,14 @@ export const claudeAdapter: AIAdapter = { } await readSSE(res, (data) => { - const parsed = JSON.parse(data) - if (parsed.type === 'content_block_delta' && parsed.delta?.text) { - onToken(parsed.delta.text) - } else if (parsed.type === 'error') { - onError(parsed.error?.message ?? 'Claude stream error') - } + try { + const parsed = JSON.parse(data) + if (parsed.type === 'content_block_delta' && parsed.delta?.text) { + onToken(parsed.delta.text) + } else if (parsed.type === 'error') { + onError(parsed.error?.message ?? 'Claude stream error') + } + } catch { /* malformed SSE chunk */ } }, onError, options.signal) }, } diff --git a/packages/app/src/adapters/openrouter-adapter.ts b/packages/app/src/adapters/openrouter-adapter.ts index ac2ee05b..8bfec97e 100644 --- a/packages/app/src/adapters/openrouter-adapter.ts +++ b/packages/app/src/adapters/openrouter-adapter.ts @@ -60,9 +60,11 @@ export const openrouterAdapter: AIAdapter = { await readSSE(res, (data) => { if (data === '[DONE]') return - const parsed = JSON.parse(data) - const delta = parsed.choices?.[0]?.delta?.content - if (delta) onToken(delta) + try { + const parsed = JSON.parse(data) + const delta = parsed.choices?.[0]?.delta?.content + if (delta) onToken(delta) + } catch { /* malformed SSE chunk */ } }, onError, options.signal) }, } diff --git a/packages/app/src/composables/useAI.ts b/packages/app/src/composables/useAI.ts index df2af9f6..9108e561 100644 --- a/packages/app/src/composables/useAI.ts +++ b/packages/app/src/composables/useAI.ts @@ -209,12 +209,14 @@ async function streamClaude( } await readSSE(res, (data) => { - const parsed = JSON.parse(data) - if (parsed.type === 'content_block_delta' && parsed.delta?.text) { - onToken(parsed.delta.text) - } else if (parsed.type === 'error') { - onError(parsed.error?.message ?? 'Claude stream error') - } + try { + const parsed = JSON.parse(data) + if (parsed.type === 'content_block_delta' && parsed.delta?.text) { + onToken(parsed.delta.text) + } else if (parsed.type === 'error') { + onError(parsed.error?.message ?? 'Claude stream error') + } + } catch { /* malformed SSE chunk */ } }, onError, signal) } @@ -261,9 +263,11 @@ async function streamOpenRouter( await readSSE(res, (data) => { if (data === '[DONE]') return - const parsed = JSON.parse(data) - const delta = parsed.choices?.[0]?.delta?.content - if (delta) onToken(delta) + try { + const parsed = JSON.parse(data) + const delta = parsed.choices?.[0]?.delta?.content + if (delta) onToken(delta) + } catch { /* malformed SSE chunk */ } }, onError, signal) } diff --git a/packages/app/src/composables/useSimilarContent.ts b/packages/app/src/composables/useSimilarContent.ts index c5466ad8..6e4cb53d 100644 --- a/packages/app/src/composables/useSimilarContent.ts +++ b/packages/app/src/composables/useSimilarContent.ts @@ -78,7 +78,10 @@ export function useSimilarContent() { // Extract JSON array from response const match = text.match(/\[[\s\S]*\]/) if (match) { - const parsed = JSON.parse(match[0]) as { title: string; reason: string }[] + let parsed: { title: string; reason: string }[] + try { + parsed = JSON.parse(match[0]) as { title: string; reason: string }[] + } catch { return } const items: SimilarItem[] = parsed.slice(0, 3).map(p => ({ title: p.title, type,