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.
This commit is contained in:
Teknium
2026-09-11 01:38:04 -07:00
parent a6ee31f55a
commit 66a13703b3
3 changed files with 52 additions and 3 deletions

View File

@@ -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
}

View File

@@ -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

View File

@@ -215,6 +215,7 @@ function IdleView({
</Button>
}
body={u.connectionRetry}
detail={status.message}
icon={<ErrorIcon />}
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({
<DialogTitle className="text-center text-lg">{title}</DialogTitle>
{body && <DialogDescription className="text-center text-sm">{body}</DialogDescription>}
{detail && (
<p className="max-w-sm break-words rounded-md bg-muted/40 px-2 py-1 font-mono text-xs text-muted-foreground">
{detail}
</p>
)}
</div>
{action && <div className="flex justify-center">{action}</div>}