Skip to content

Commit 13e95b9

Browse files
authored
πŸ› fix(desktop): gracefully handle missing update manifest 404 errors (#11625)
- Add isMissingUpdateManifestError helper to detect manifest 404 errors - Treat missing manifest as "no update available" during gap period - Fix sidebar header margin for desktop layout
1 parent 01550e0 commit 13e95b9

File tree

3 files changed

+96
-3
lines changed

3 files changed

+96
-3
lines changed

β€Žapps/desktop/src/main/core/infrastructure/UpdaterManager.tsβ€Ž

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { UpdateInfo } from '@lobechat/electron-client-ipc';
2+
import { app as electronApp } from 'electron';
13
import log from 'electron-log';
24
import { autoUpdater } from 'electron-updater';
35

@@ -120,10 +122,22 @@ export class UpdaterManager {
120122
try {
121123
await autoUpdater.checkForUpdates();
122124
} catch (error) {
123-
logger.error('Error checking for updates:', error.message);
125+
const message = error instanceof Error ? error.message : String(error);
126+
127+
// Edge case: Release tag exists but update manifest assets (latest/stable-*.yml) aren't uploaded yet.
128+
// Treat this gap period as "no updates available" instead of a user-facing error.
129+
if (this.isMissingUpdateManifestError(error)) {
130+
logger.warn('[Updater] Update manifest not ready yet, treating as no update:', message);
131+
if (manual) {
132+
this.mainWindow.broadcast('manualUpdateNotAvailable', this.getCurrentUpdateInfo());
133+
}
134+
return;
135+
}
136+
137+
logger.error('Error checking for updates:', message);
124138

125139
if (manual) {
126-
this.mainWindow.broadcast('updateError', (error as Error).message);
140+
this.mainWindow.broadcast('updateError', message);
127141
}
128142
} finally {
129143
this.checking = false;
@@ -397,6 +411,18 @@ export class UpdaterManager {
397411
});
398412

399413
autoUpdater.on('error', async (err) => {
414+
const message = err instanceof Error ? err.message : String(err);
415+
416+
// Edge case: Release tag exists but update manifest assets aren't uploaded yet.
417+
// Skip fallback switching and avoid user-facing errors.
418+
if (this.isMissingUpdateManifestError(err)) {
419+
logger.warn('[Updater] Update manifest not ready yet, skipping error handling:', message);
420+
if (this.isManualCheck) {
421+
this.mainWindow.broadcast('manualUpdateNotAvailable', this.getCurrentUpdateInfo());
422+
}
423+
return;
424+
}
425+
400426
logger.error('Error in auto-updater:', err);
401427
logger.error('[Updater Error Context] Channel:', autoUpdater.channel);
402428
logger.error('[Updater Error Context] allowPrerelease:', autoUpdater.allowPrerelease);
@@ -436,4 +462,28 @@ export class UpdaterManager {
436462

437463
logger.debug('Updater events registered');
438464
}
465+
466+
private isMissingUpdateManifestError(error: unknown): boolean {
467+
const message = error instanceof Error ? error.message : String(error ?? '');
468+
if (!message) return false;
469+
470+
// Expect patterns like:
471+
// - "Cannot find latest-mac.yml ... HttpError: 404 ..."
472+
// - "Cannot find stable.yml ... 404 ..."
473+
if (!/cannot find/i.test(message)) return false;
474+
if (!/\b404\b/.test(message)) return false;
475+
476+
// Match channel manifest filenames across platforms/architectures:
477+
// latest.yml, latest-mac.yml, latest-linux.yml, stable.yml, stable-mac.yml, etc.
478+
const manifestMatch = message.match(/\b(?:latest|stable)(?:-[\da-z]+)?\.yml\b/i);
479+
return Boolean(manifestMatch);
480+
}
481+
482+
private getCurrentUpdateInfo(): UpdateInfo {
483+
const version = autoUpdater.currentVersion?.version || electronApp.getVersion();
484+
return {
485+
releaseDate: new Date().toISOString(),
486+
version,
487+
};
488+
}
439489
}

β€Žapps/desktop/src/main/core/infrastructure/__tests__/UpdaterManager.test.tsβ€Ž

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ vi.mock('electron-updater', () => ({
3131
autoInstallOnAppQuit: false,
3232
channel: 'stable',
3333
checkForUpdates: vi.fn(),
34+
currentVersion: undefined as any,
3435
downloadUpdate: vi.fn(),
3536
forceDevUpdateConfig: false,
3637
logger: null as any,
@@ -46,6 +47,7 @@ vi.mock('electron', () => ({
4647
getAllWindows: mockGetAllWindows,
4748
},
4849
app: {
50+
getVersion: vi.fn().mockReturnValue('0.0.0'),
4951
releaseSingleInstanceLock: mockReleaseSingleInstanceLock,
5052
},
5153
}));
@@ -108,6 +110,7 @@ describe('UpdaterManager', () => {
108110
(autoUpdater as any).allowPrerelease = false;
109111
(autoUpdater as any).allowDowngrade = false;
110112
(autoUpdater as any).forceDevUpdateConfig = false;
113+
(autoUpdater as any).currentVersion = undefined;
111114

112115
// Capture registered events
113116
registeredEvents = new Map();
@@ -212,6 +215,24 @@ describe('UpdaterManager', () => {
212215

213216
expect(mockBroadcast).toHaveBeenCalledWith('updateError', 'Network error');
214217
});
218+
219+
it('should treat missing latest/stable yml 404 as not-available during manual check', async () => {
220+
const error = new Error(
221+
'Cannot find latest-mac.yml in the latest release artifacts (https://github.com/lobehub/lobe-chat/releases/download/v2.0.0-next.311/latest-mac.yml): HttpError: 404',
222+
);
223+
vi.mocked(autoUpdater.checkForUpdates).mockRejectedValueOnce(error);
224+
225+
await updaterManager.checkForUpdates({ manual: true });
226+
227+
expect(mockBroadcast).toHaveBeenCalledWith(
228+
'manualUpdateNotAvailable',
229+
expect.objectContaining({
230+
releaseDate: expect.any(String),
231+
version: expect.any(String),
232+
}),
233+
);
234+
expect(mockBroadcast).not.toHaveBeenCalledWith('updateError', expect.anything());
235+
});
215236
});
216237

217238
describe('downloadUpdate', () => {
@@ -486,6 +507,26 @@ describe('UpdaterManager', () => {
486507

487508
expect(mockBroadcast).not.toHaveBeenCalledWith('updateError', expect.anything());
488509
});
510+
511+
it('should not broadcast updateError for missing manifest 404 (gap period)', async () => {
512+
vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
513+
await updaterManager.checkForUpdates({ manual: true });
514+
515+
const error = new Error(
516+
'Cannot find latest-mac.yml in the latest release artifacts (https://github.com/lobehub/lobe-chat/releases/download/v2.0.0-next.311/latest-mac.yml): HttpError: 404',
517+
);
518+
const handler = registeredEvents.get('error');
519+
await handler?.(error);
520+
521+
expect(mockBroadcast).toHaveBeenCalledWith(
522+
'manualUpdateNotAvailable',
523+
expect.objectContaining({
524+
releaseDate: expect.any(String),
525+
version: expect.any(String),
526+
}),
527+
);
528+
expect(mockBroadcast).not.toHaveBeenCalledWith('updateError', expect.anything());
529+
});
489530
});
490531
});
491532

β€Žsrc/features/NavPanel/SideBarHeaderLayout.tsxβ€Ž

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { type ReactNode, memo } from 'react';
88
import { flushSync } from 'react-dom';
99
import { useNavigate } from 'react-router-dom';
1010

11+
import { isDesktop } from '@/const/version';
12+
1113
import ToggleLeftPanelButton from './ToggleLeftPanelButton';
1214
import BackButton from './components/BackButton';
1315

@@ -35,7 +37,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
3537
`,
3638
container: css`
3739
overflow: hidden;
38-
margin-block-start: 8px;
40+
margin-block-start: ${isDesktop ? '' : '8px'};
3941
`,
4042
}));
4143

0 commit comments

Comments
Β (0)