Skip to content

Commit 6d9badc

Browse files
liangweifengclaude
andcommitted
fix: user-friendly error messages for API failures
Cross-file error flow audit → fixes: 1. STT layer: parseApiError() extracts human-readable message from JSON error bodies (e.g., "Rate limit exceeded" instead of raw JSON blob) 2. ResultPanel: friendlyErrorMessage() now handles HTTP error codes: - 401/403 → "Authentication Failed" (check your API key) - 429 → "Rate Limited" (wait and retry) - 5xx → "Service Unavailable" (try later) 3. Realtime STT: onError now sends 'error' phase to overlay via IPC (was silently swallowed with only console.error) 4. i18n: 6 new keys in en.json + zh.json for error messages Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 244ac4c commit 6d9badc

5 files changed

Lines changed: 36 additions & 6 deletions

File tree

electron/ipc-handlers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ export function setupIPC() {
155155
};
156156
session.onError = (error) => {
157157
console.error('[RealtimeSTT] error:', error);
158+
if (overlayWC && !overlayWC.isDestroyed()) {
159+
overlayWC.send('pipeline:phase', 'error');
160+
}
158161
};
159162
await session.connect();
160163
// If user cancelled during connect, don't publish the session

electron/stt-service.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,16 @@ class ParaformerRealtimeSession implements IRealtimeSession {
460460
}
461461
}
462462

463+
/** Extract human-readable error from API response body (JSON or plain text) */
464+
function parseApiError(status: number, body: string): string {
465+
try {
466+
const json = JSON.parse(body);
467+
const msg = json?.error?.message || json?.message || json?.error || '';
468+
if (msg) return `${status}: ${typeof msg === 'string' ? msg : JSON.stringify(msg)}`;
469+
} catch {}
470+
return `${status}: ${body.slice(0, 200)}`;
471+
}
472+
463473
// ═════════════════════════════════════════════════════════════════════════════
464474
// STTService — public API
465475
// ═════════════════════════════════════════════════════════════════════════════
@@ -522,8 +532,7 @@ export class STTService {
522532
});
523533

524534
if (!res.ok) {
525-
const err = await res.text();
526-
throw new Error(`DashScope STT ${res.status}: ${err.slice(0, 300)}`);
535+
throw new Error(`DashScope STT ${parseApiError(res.status, await res.text().catch(() => ''))}`);
527536
}
528537

529538
const json = await res.json();
@@ -555,8 +564,7 @@ export class STTService {
555564
});
556565

557566
if (!res.ok) {
558-
const err = await res.text();
559-
throw new Error(`STT ${res.status}: ${err.slice(0, 300)}`);
567+
throw new Error(`STT ${parseApiError(res.status, await res.text().catch(() => ''))}`);
560568
}
561569

562570
const json = await res.json();

src/components/recording/ResultPanel.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ function friendlyErrorMessage(error: string, t: (k: string) => string): { title:
1616
return { title: t('recording.errorTimeout'), detail: t('recording.errorTimeoutDetail') };
1717
if (lower.includes('pipeline busy'))
1818
return { title: t('recording.errorBusy'), detail: t('recording.errorBusyDetail') };
19+
// HTTP status code errors from API calls
20+
if (lower.includes('401') || lower.includes('403') || lower.includes('unauthorized') || lower.includes('forbidden') || lower.includes('invalid'))
21+
return { title: t('recording.errorAuth'), detail: t('recording.errorAuthDetail') };
22+
if (lower.includes('429') || lower.includes('rate limit'))
23+
return { title: t('recording.errorRateLimit'), detail: t('recording.errorRateLimitDetail') };
24+
if (/\b5\d\d\b/.test(error) || lower.includes('service unavailable') || lower.includes('bad gateway'))
25+
return { title: t('recording.errorServer'), detail: t('recording.errorServerDetail') };
1926
// Fallback: show technical error
2027
return { title: t('recording.error'), detail: error };
2128
}

src/i18n/locales/en.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,13 @@
156156
"errorTimeout": "Request Timed Out",
157157
"errorTimeoutDetail": "The API took too long to respond. Please try again.",
158158
"errorBusy": "Processing In Progress",
159-
"errorBusyDetail": "Please wait for the current transcription to finish."
159+
"errorBusyDetail": "Please wait for the current transcription to finish.",
160+
"errorAuth": "Authentication Failed",
161+
"errorAuthDetail": "Your API key may be invalid or expired. Please check it in Settings.",
162+
"errorRateLimit": "Rate Limited",
163+
"errorRateLimitDetail": "Too many requests. Please wait a moment and try again.",
164+
"errorServer": "Service Unavailable",
165+
"errorServerDetail": "The API service is temporarily down. Please try again later."
160166
},
161167
"overlay": {
162168
"listening": "Listening...",

src/i18n/locales/zh.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,13 @@
156156
"errorTimeout": "请求超时",
157157
"errorTimeoutDetail": "API 响应时间过长,请稍后重试。",
158158
"errorBusy": "正在处理中",
159-
"errorBusyDetail": "请等待当前转录完成。"
159+
"errorBusyDetail": "请等待当前转录完成。",
160+
"errorAuth": "认证失败",
161+
"errorAuthDetail": "API 密钥可能无效或已过期,请在设置中检查。",
162+
"errorRateLimit": "请求频率过高",
163+
"errorRateLimitDetail": "请求太频繁,请稍等片刻再试。",
164+
"errorServer": "服务暂不可用",
165+
"errorServerDetail": "API 服务暂时故障,请稍后再试。"
160166
},
161167
"overlay": {
162168
"listening": "正在收听...",

0 commit comments

Comments
 (0)