mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
🐛 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
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import type { UpdateInfo } from '@lobechat/electron-client-ipc';
|
||||
import { app as electronApp } from 'electron';
|
||||
import log from 'electron-log';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
|
||||
@@ -120,10 +122,22 @@ export class UpdaterManager {
|
||||
try {
|
||||
await autoUpdater.checkForUpdates();
|
||||
} catch (error) {
|
||||
logger.error('Error checking for updates:', error.message);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// Edge case: Release tag exists but update manifest assets (latest/stable-*.yml) aren't uploaded yet.
|
||||
// Treat this gap period as "no updates available" instead of a user-facing error.
|
||||
if (this.isMissingUpdateManifestError(error)) {
|
||||
logger.warn('[Updater] Update manifest not ready yet, treating as no update:', message);
|
||||
if (manual) {
|
||||
this.mainWindow.broadcast('manualUpdateNotAvailable', this.getCurrentUpdateInfo());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error('Error checking for updates:', message);
|
||||
|
||||
if (manual) {
|
||||
this.mainWindow.broadcast('updateError', (error as Error).message);
|
||||
this.mainWindow.broadcast('updateError', message);
|
||||
}
|
||||
} finally {
|
||||
this.checking = false;
|
||||
@@ -397,6 +411,18 @@ export class UpdaterManager {
|
||||
});
|
||||
|
||||
autoUpdater.on('error', async (err) => {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
|
||||
// Edge case: Release tag exists but update manifest assets aren't uploaded yet.
|
||||
// Skip fallback switching and avoid user-facing errors.
|
||||
if (this.isMissingUpdateManifestError(err)) {
|
||||
logger.warn('[Updater] Update manifest not ready yet, skipping error handling:', message);
|
||||
if (this.isManualCheck) {
|
||||
this.mainWindow.broadcast('manualUpdateNotAvailable', this.getCurrentUpdateInfo());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error('Error in auto-updater:', err);
|
||||
logger.error('[Updater Error Context] Channel:', autoUpdater.channel);
|
||||
logger.error('[Updater Error Context] allowPrerelease:', autoUpdater.allowPrerelease);
|
||||
@@ -436,4 +462,28 @@ export class UpdaterManager {
|
||||
|
||||
logger.debug('Updater events registered');
|
||||
}
|
||||
|
||||
private isMissingUpdateManifestError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error ?? '');
|
||||
if (!message) return false;
|
||||
|
||||
// Expect patterns like:
|
||||
// - "Cannot find latest-mac.yml ... HttpError: 404 ..."
|
||||
// - "Cannot find stable.yml ... 404 ..."
|
||||
if (!/cannot find/i.test(message)) return false;
|
||||
if (!/\b404\b/.test(message)) return false;
|
||||
|
||||
// Match channel manifest filenames across platforms/architectures:
|
||||
// latest.yml, latest-mac.yml, latest-linux.yml, stable.yml, stable-mac.yml, etc.
|
||||
const manifestMatch = message.match(/\b(?:latest|stable)(?:-[\da-z]+)?\.yml\b/i);
|
||||
return Boolean(manifestMatch);
|
||||
}
|
||||
|
||||
private getCurrentUpdateInfo(): UpdateInfo {
|
||||
const version = autoUpdater.currentVersion?.version || electronApp.getVersion();
|
||||
return {
|
||||
releaseDate: new Date().toISOString(),
|
||||
version,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ vi.mock('electron-updater', () => ({
|
||||
autoInstallOnAppQuit: false,
|
||||
channel: 'stable',
|
||||
checkForUpdates: vi.fn(),
|
||||
currentVersion: undefined as any,
|
||||
downloadUpdate: vi.fn(),
|
||||
forceDevUpdateConfig: false,
|
||||
logger: null as any,
|
||||
@@ -46,6 +47,7 @@ vi.mock('electron', () => ({
|
||||
getAllWindows: mockGetAllWindows,
|
||||
},
|
||||
app: {
|
||||
getVersion: vi.fn().mockReturnValue('0.0.0'),
|
||||
releaseSingleInstanceLock: mockReleaseSingleInstanceLock,
|
||||
},
|
||||
}));
|
||||
@@ -108,6 +110,7 @@ describe('UpdaterManager', () => {
|
||||
(autoUpdater as any).allowPrerelease = false;
|
||||
(autoUpdater as any).allowDowngrade = false;
|
||||
(autoUpdater as any).forceDevUpdateConfig = false;
|
||||
(autoUpdater as any).currentVersion = undefined;
|
||||
|
||||
// Capture registered events
|
||||
registeredEvents = new Map();
|
||||
@@ -212,6 +215,24 @@ describe('UpdaterManager', () => {
|
||||
|
||||
expect(mockBroadcast).toHaveBeenCalledWith('updateError', 'Network error');
|
||||
});
|
||||
|
||||
it('should treat missing latest/stable yml 404 as not-available during manual check', async () => {
|
||||
const error = new Error(
|
||||
'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',
|
||||
);
|
||||
vi.mocked(autoUpdater.checkForUpdates).mockRejectedValueOnce(error);
|
||||
|
||||
await updaterManager.checkForUpdates({ manual: true });
|
||||
|
||||
expect(mockBroadcast).toHaveBeenCalledWith(
|
||||
'manualUpdateNotAvailable',
|
||||
expect.objectContaining({
|
||||
releaseDate: expect.any(String),
|
||||
version: expect.any(String),
|
||||
}),
|
||||
);
|
||||
expect(mockBroadcast).not.toHaveBeenCalledWith('updateError', expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadUpdate', () => {
|
||||
@@ -486,6 +507,26 @@ describe('UpdaterManager', () => {
|
||||
|
||||
expect(mockBroadcast).not.toHaveBeenCalledWith('updateError', expect.anything());
|
||||
});
|
||||
|
||||
it('should not broadcast updateError for missing manifest 404 (gap period)', async () => {
|
||||
vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
|
||||
await updaterManager.checkForUpdates({ manual: true });
|
||||
|
||||
const error = new Error(
|
||||
'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',
|
||||
);
|
||||
const handler = registeredEvents.get('error');
|
||||
await handler?.(error);
|
||||
|
||||
expect(mockBroadcast).toHaveBeenCalledWith(
|
||||
'manualUpdateNotAvailable',
|
||||
expect.objectContaining({
|
||||
releaseDate: expect.any(String),
|
||||
version: expect.any(String),
|
||||
}),
|
||||
);
|
||||
expect(mockBroadcast).not.toHaveBeenCalledWith('updateError', expect.anything());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import { type ReactNode, memo } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { isDesktop } from '@/const/version';
|
||||
|
||||
import ToggleLeftPanelButton from './ToggleLeftPanelButton';
|
||||
import BackButton from './components/BackButton';
|
||||
|
||||
@@ -35,7 +37,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
`,
|
||||
container: css`
|
||||
overflow: hidden;
|
||||
margin-block-start: 8px;
|
||||
margin-block-start: ${isDesktop ? '' : '8px'};
|
||||
`,
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user