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={
+ {detail} +
+ )} {action &&