From 66a13703b3cebb9270ec1008c01d8dfabb5c306f Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 11 Sep 2026 01:38:04 -0700 Subject: [PATCH] fix(desktop): update-check failures name the real cause instead of 'couldn't reach the update server' A GitHub outage, a rate limit, a corporate proxy intercepting TLS and a DNS failure all rendered as the same generic line, so #105855 read as a Hermes bug during a run of GitHub incidents. The main process now classifies the failure (HTTP status incl. 403/429 rate-limit and 5xx outage wording, DNS, timeout, connection refused/reset, TLS) into one actionable sentence; the overlay shows it under the title and About appends it to the status line. --- apps/desktop/electron/main.ts | 43 ++++++++++++++++++- .../src/app/settings/about-settings.tsx | 2 +- apps/desktop/src/app/updates-overlay.tsx | 10 +++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 423cdffcd8..bb26dd487d 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -3257,7 +3257,7 @@ async function checkUpdatesViaApi({ slug, branch, currentSha }) { try { targetSha = String(await fetchGitHubApi(branchTipApiUrl(slug, branch), 'application/vnd.github.sha')).trim() } catch (error) { - return { error: 'fetch-failed', message: `GitHub API: ${error?.message || error}` } + return { error: 'fetch-failed', message: describeUpdateCheckFailure(error) } } if (!/^[0-9a-f]{40}$/i.test(targetSha)) { @@ -3315,6 +3315,45 @@ async function checkUpdatesViaLsRemote({ updateRoot, branch, currentSha }) { return { behind: null, updateAvailable: true, targetSha, commits: [] } } +// One line a user can act on (or paste into a bug report) instead of the +// generic "couldn't reach the update server": which host, which failure. +// #105855 was a run of GitHub outages that read as a Hermes bug because the +// UI hid the cause. +function describeUpdateCheckFailure(error) { + const status = error?.statusCode + const code = error?.code + + if (status === 403 || status === 429) { + return `GitHub API rate limit reached (HTTP ${status}) — try again in an hour.` + } + + if (typeof status === 'number' && status >= 500) { + return `GitHub is having trouble (HTTP ${status} from api.github.com) — check githubstatus.com and try again later.` + } + + if (typeof status === 'number') { + return `api.github.com answered HTTP ${status}.` + } + + if (code === 'ENOTFOUND' || code === 'EAI_AGAIN') { + return 'DNS lookup for api.github.com failed — check your connection or proxy.' + } + + if (code === 'ETIMEDOUT' || error?.message === 'timeout') { + return 'api.github.com did not answer within 10 seconds.' + } + + if (code === 'ECONNREFUSED' || code === 'ECONNRESET' || code === 'EHOSTUNREACH' || code === 'ENETUNREACH') { + return `Connection to api.github.com failed (${code}) — a firewall or proxy may be blocking it.` + } + + if (typeof code === 'string' && /CERT|SSL|TLS/i.test(code)) { + return `TLS handshake with api.github.com failed (${code}) — a proxy may be intercepting HTTPS.` + } + + return `api.github.com: ${error?.message || String(error)}` +} + function fetchGitHubApi(url, accept = 'application/vnd.github+json') { return new Promise((resolve, reject) => { const req = https.get( @@ -3335,7 +3374,7 @@ function fetchGitHubApi(url, accept = 'application/vnd.github+json') { const body = Buffer.concat(chunks).toString('utf8') if ((res.statusCode || 500) >= 400) { - reject(new Error(`HTTP ${res.statusCode}`)) + reject(Object.assign(new Error(`HTTP ${res.statusCode}`), { statusCode: res.statusCode })) return } diff --git a/apps/desktop/src/app/settings/about-settings.tsx b/apps/desktop/src/app/settings/about-settings.tsx index c60a4b2ca9..81f797078c 100644 --- a/apps/desktop/src/app/settings/about-settings.tsx +++ b/apps/desktop/src/app/settings/about-settings.tsx @@ -83,7 +83,7 @@ export function AboutSettings() { statusLine = status?.message ?? a.cantUpdate statusTone = 'error' } else if (status?.error) { - statusLine = a.cantReach + statusLine = status.message ? `${a.cantReach} ${status.message}` : a.cantReach statusTone = 'error' } else if (applying) { statusLine = a.installing diff --git a/apps/desktop/src/app/updates-overlay.tsx b/apps/desktop/src/app/updates-overlay.tsx index a566f1b176..23912bde34 100644 --- a/apps/desktop/src/app/updates-overlay.tsx +++ b/apps/desktop/src/app/updates-overlay.tsx @@ -215,6 +215,7 @@ function IdleView({ } body={u.connectionRetry} + detail={status.message} icon={} title={u.checkFailedTitle} /> @@ -557,11 +558,15 @@ function ErrorView({ message, onDismiss, onRetry }: { message: string; onDismiss function CenteredStatus({ action, body, + detail, icon, title }: { action?: React.ReactNode body?: string + /** Diagnostic line from the main process (HTTP status, DNS, TLS…), shown + * verbatim so a bug report carries the real cause. */ + detail?: string icon: React.ReactNode title: string }) { @@ -572,6 +577,11 @@ function CenteredStatus({ {title} {body && {body}} + {detail && ( +

+ {detail} +

+ )} {action &&
{action}
}