From 4926e3579fc6ad44245e49e62af756a58dd577e7 Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Sun, 20 Jul 2025 15:16:27 +0200 Subject: [PATCH] Catch json errors a little better (#3437) Co-authored-by: Douwe Osinga --- .../components/settings/app/AppSettingsSection.tsx | 2 +- ui/desktop/src/extensions.tsx | 3 ++- ui/desktop/src/hooks/useWhisper.ts | 14 ++++++++++---- ui/desktop/src/recipe/index.ts | 3 ++- ui/desktop/src/sharedSessions.ts | 11 +++++++++-- ui/desktop/src/utils/askAI.ts | 3 ++- ui/desktop/src/utils/costDatabase.ts | 11 ++++++++++- ui/desktop/src/utils/githubUpdater.ts | 6 +++++- ui/desktop/src/utils/jsonUtils.ts | 13 +++++++++++++ 9 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 ui/desktop/src/utils/jsonUtils.ts diff --git a/ui/desktop/src/components/settings/app/AppSettingsSection.tsx b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx index eb75e2c0..284b5cfa 100644 --- a/ui/desktop/src/components/settings/app/AppSettingsSection.tsx +++ b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx @@ -85,7 +85,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti }); if (response.ok) { - await response.json(); // Consume the response + await response.json(); setPricingStatus('success'); setLastFetchTime(new Date()); } else { diff --git a/ui/desktop/src/extensions.tsx b/ui/desktop/src/extensions.tsx index 5c314dec..9f1e01a3 100644 --- a/ui/desktop/src/extensions.tsx +++ b/ui/desktop/src/extensions.tsx @@ -1,5 +1,6 @@ import { getApiUrl, getSecretKey } from './config'; import { toast } from 'react-toastify'; +import { safeJsonParse } from './utils/jsonUtils'; import builtInExtensionsData from './built-in-extensions.json'; import { toastError, toastLoading, toastSuccess } from './toasts'; @@ -181,7 +182,7 @@ export async function removeExtension(name: string, silent: boolean = false): Pr body: JSON.stringify(sanitizeName(name)), }); - const data = await response.json(); + const data = await safeJsonParse<{ error: boolean; message: string }>(response); if (!data.error) { if (!silent) { diff --git a/ui/desktop/src/hooks/useWhisper.ts b/ui/desktop/src/hooks/useWhisper.ts index 6e49e616..199ce192 100644 --- a/ui/desktop/src/hooks/useWhisper.ts +++ b/ui/desktop/src/hooks/useWhisper.ts @@ -2,6 +2,7 @@ import { useState, useRef, useCallback, useEffect } from 'react'; import { useConfig } from '../components/ConfigContext'; import { getApiUrl, getSecretKey } from '../config'; import { useDictationSettings } from './useDictationSettings'; +import { safeJsonParse } from '../utils/jsonUtils'; interface UseWhisperOptions { onTranscription?: (text: string) => void; @@ -151,13 +152,18 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp } else if (response.status === 402) { throw new Error('API quota exceeded. Please check your account limits.'); } - const errorData = await response - .json() - .catch(() => ({ error: { message: 'Transcription failed' } })); + const errorData = await safeJsonParse<{ + error: { message: string }; + }>(response, 'Failed to parse error response').catch(() => ({ + error: { message: 'Transcription failed' }, + })); throw new Error(errorData.error?.message || 'Transcription failed'); } - const data = await response.json(); + const data = await safeJsonParse<{ text: string }>( + response, + 'Failed to parse transcription response' + ); if (data.text) { onTranscription?.(data.text); } diff --git a/ui/desktop/src/recipe/index.ts b/ui/desktop/src/recipe/index.ts index 8158fed0..12cf7962 100644 --- a/ui/desktop/src/recipe/index.ts +++ b/ui/desktop/src/recipe/index.ts @@ -1,6 +1,7 @@ import { Message } from '../types/message'; import { getApiUrl } from '../config'; import { FullExtensionConfig } from '../extensions'; +import { safeJsonParse } from '../utils/jsonUtils'; export interface Parameter { key: string; @@ -70,7 +71,7 @@ export async function createRecipe(request: CreateRecipeRequest): Promise(response, 'Server failed to create recipe:'); } export interface EncodeRecipeRequest { diff --git a/ui/desktop/src/sharedSessions.ts b/ui/desktop/src/sharedSessions.ts index 839e1e1d..2dd469aa 100644 --- a/ui/desktop/src/sharedSessions.ts +++ b/ui/desktop/src/sharedSessions.ts @@ -1,4 +1,5 @@ import { Message } from './types/message'; +import { safeJsonParse } from './utils/jsonUtils'; export interface SharedSessionDetails { share_token: string; @@ -35,7 +36,10 @@ export async function fetchSharedSessionDetails( throw new Error(`Failed to fetch shared session: ${response.status} ${response.statusText}`); } - const data = await response.json(); + const data = await safeJsonParse( + response, + 'Failed to parse shared session' + ); if (baseUrl != data.base_url) { throw new Error(`Base URL mismatch for shared session: ${baseUrl} != ${data.base_url}`); @@ -98,7 +102,10 @@ export async function createSharedSession( throw new Error(`Failed to create shared session: ${response.status} ${response.statusText}`); } - const data = await response.json(); + const data = await safeJsonParse<{ share_token: string }>( + response, + 'Failed to parse shared session response' + ); return data.share_token; } catch (error) { console.error('Error creating shared session:', error); diff --git a/ui/desktop/src/utils/askAI.ts b/ui/desktop/src/utils/askAI.ts index 1d2f3265..bdf7a0d2 100644 --- a/ui/desktop/src/utils/askAI.ts +++ b/ui/desktop/src/utils/askAI.ts @@ -1,4 +1,5 @@ import { getApiUrl, getSecretKey } from '../config'; +import { safeJsonParse } from './jsonUtils'; const getQuestionClassifierPrompt = (messageContent: string): string => ` You are a simple classifier that takes content and decides if it is asking for input @@ -167,7 +168,7 @@ export async function ask(prompt: string): Promise { throw new Error('Failed to get response'); } - const data = await response.json(); + const data = await safeJsonParse<{ response: string }>(response, 'Failed to get AI response'); return data.response; } diff --git a/ui/desktop/src/utils/costDatabase.ts b/ui/desktop/src/utils/costDatabase.ts index 82c684bc..137ae13c 100644 --- a/ui/desktop/src/utils/costDatabase.ts +++ b/ui/desktop/src/utils/costDatabase.ts @@ -1,5 +1,6 @@ // Import the proper type from ConfigContext import { getApiUrl, getSecretKey } from '../config'; +import { safeJsonParse } from './jsonUtils'; export interface ModelCostInfo { input_token_cost: number; // Cost per token for input (in USD) @@ -47,7 +48,15 @@ async function fetchPricingForModel( throw new Error(`API request failed with status ${response.status}`); } - const data = await response.json(); + const data = await safeJsonParse<{ + pricing: Array<{ + provider: string; + model: string; + input_token_cost: number; + output_token_cost: number; + currency: string; + }>; + }>(response, 'Failed to parse pricing data'); // Find the specific model pricing using the lookup provider/model const pricing = data.pricing?.find( diff --git a/ui/desktop/src/utils/githubUpdater.ts b/ui/desktop/src/utils/githubUpdater.ts index f9cdebaa..9aa784cd 100644 --- a/ui/desktop/src/utils/githubUpdater.ts +++ b/ui/desktop/src/utils/githubUpdater.ts @@ -4,6 +4,7 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; import log from './logger'; +import { safeJsonParse } from './jsonUtils'; interface GitHubRelease { tag_name: string; @@ -53,7 +54,10 @@ export class GitHubUpdater { throw new Error(`GitHub API returned ${response.status}: ${response.statusText}`); } - const release: GitHubRelease = await response.json(); + const release: GitHubRelease = await safeJsonParse( + response, + 'Failed to get GitHub release information' + ); log.info(`GitHubUpdater: Found release: ${release.tag_name} (${release.name})`); log.info(`GitHubUpdater: Release published at: ${release.published_at}`); log.info(`GitHubUpdater: Release assets count: ${release.assets.length}`); diff --git a/ui/desktop/src/utils/jsonUtils.ts b/ui/desktop/src/utils/jsonUtils.ts new file mode 100644 index 00000000..1f8f3ec0 --- /dev/null +++ b/ui/desktop/src/utils/jsonUtils.ts @@ -0,0 +1,13 @@ +export async function safeJsonParse( + response: Response, + errorMessage: string = 'Failed to parse server response' +): Promise { + try { + return (await response.json()) as T; + } catch (error) { + if (error instanceof SyntaxError) { + throw new Error(errorMessage); + } + throw error; + } +}