fix(updater): never throw from an auto update check (#5028)

checkForAppUpdates runs fire-and-forget on reader/library mount, but it threw on
any network failure: the desktop check() propagated "error sending request for
url (...latest.json)" (READEST-J) and the Android branch re-threw "Failed to
fetch Android update info" (READEST-22). Both surfaced as unhandled rejections.
Wrap the check so a failure is swallowed (returns false) for an auto-check;
manual checks (About window) still surface the error. Adds regression tests.

Fixes READEST-J
Fixes READEST-22

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin 2026-07-09 16:10:27 +09:00 committed by GitHub
parent fdd13a5a6a
commit 15cca23773
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 71 additions and 39 deletions

View file

@ -256,6 +256,24 @@ describe('updater', () => {
);
});
test('auto-check swallows Android update failure (READEST-22)', async () => {
mockOsType.mockReturnValue('android');
mockTauriFetch.mockRejectedValue(new Error('Network error'));
// Auto-check runs fire-and-forget on mount; a network failure must resolve
// false, not reject (which would become an unhandled rejection).
await expect(checkForAppUpdates(dummyTranslate, true)).resolves.toBe(false);
});
test('auto-check swallows desktop update failure (READEST-J)', async () => {
mockOsType.mockReturnValue('macos');
mockCheck.mockRejectedValue(
new Error('error sending request for url (releases/latest/download/latest.json)'),
);
await expect(checkForAppUpdates(dummyTranslate, true)).resolves.toBe(false);
});
test('returns false for unsupported OS types', async () => {
mockOsType.mockReturnValue('ios');

View file

@ -149,48 +149,62 @@ export const checkForAppUpdates = async (
console.log('Checking for updates', { updateChannel });
const OS_TYPE = osType();
if (updateChannel === 'nightly') {
const platformKey = getNightlyPlatformKey(
OS_TYPE,
osArch(),
Boolean(process.env['NEXT_PUBLIC_PORTABLE_APP']),
Boolean((window as { __READEST_IS_APPIMAGE?: boolean }).__READEST_IS_APPIMAGE),
);
if (!platformKey) return false;
const resolved = await resolveNightlyUpdate(getAppVersion(), platformKey, fetch);
if (resolved) {
setUpdaterWindowVisible(true, resolved.version, getAppVersion(), true, resolved);
return true;
try {
if (updateChannel === 'nightly') {
const platformKey = getNightlyPlatformKey(
OS_TYPE,
osArch(),
Boolean(process.env['NEXT_PUBLIC_PORTABLE_APP']),
Boolean((window as { __READEST_IS_APPIMAGE?: boolean }).__READEST_IS_APPIMAGE),
);
if (!platformKey) return false;
const resolved = await resolveNightlyUpdate(getAppVersion(), platformKey, fetch);
if (resolved) {
setUpdaterWindowVisible(true, resolved.version, getAppVersion(), true, resolved);
return true;
}
return false;
}
if (['macos', 'windows', 'linux'].includes(OS_TYPE)) {
const update = await check();
if (update) {
// Enum ScrollBarStyle is exported as type by tauri, so it cannot be used directly.
const scrollBarStyle = (OS_TYPE === 'windows'
? 'fluentOverlay'
: 'default') as unknown as ScrollBarStyle;
showUpdateWindow(update.version, scrollBarStyle);
}
return !!update;
} else if (OS_TYPE === 'android') {
try {
const response = await fetch(READEST_UPDATER_FILE, { connectTimeout: 5000 });
const data = await response.json();
const isNewer = semver.gt(data.version, getAppVersion());
if (
isNewer &&
('android-arm64' in data.platforms || 'android-universal' in data.platforms)
) {
setUpdaterWindowVisible(true, data.version!, getAppVersion());
}
return isNewer;
} catch (err) {
console.warn('Failed to fetch Android update info', err);
throw new Error('Failed to fetch Android update info');
}
}
return false;
} catch (err) {
// Update checks are best-effort: they fail routinely when offline or when
// the release host is unreachable. An auto-check runs fire-and-forget on
// mount, so throwing here becomes an unhandled rejection (READEST-J desktop
// latest.json, READEST-22 Android update info). Only surface the failure for
// a manual check (About window).
console.warn('Update check failed', err);
if (!isAutoCheck) throw err;
return false;
}
if (['macos', 'windows', 'linux'].includes(OS_TYPE)) {
const update = await check();
if (update) {
// Enum ScrollBarStyle is exported as type by tauri, so it cannot be used directly.
const scrollBarStyle = (OS_TYPE === 'windows'
? 'fluentOverlay'
: 'default') as unknown as ScrollBarStyle;
showUpdateWindow(update.version, scrollBarStyle);
}
return !!update;
} else if (OS_TYPE === 'android') {
try {
const response = await fetch(READEST_UPDATER_FILE, { connectTimeout: 5000 });
const data = await response.json();
const isNewer = semver.gt(data.version, getAppVersion());
if (isNewer && ('android-arm64' in data.platforms || 'android-universal' in data.platforms)) {
setUpdaterWindowVisible(true, data.version!, getAppVersion());
}
return isNewer;
} catch (err) {
console.warn('Failed to fetch Android update info', err);
throw new Error('Failed to fetch Android update info');
}
}
return false;
};
const LAST_SHOWN_RELEASE_NOTES_KEY = 'lastShownReleaseNotesVersion';