Add error reporting for GitHub rate limit errors
- Add specific error handling for GitHub API rate limit (403) errors - Create RateLimitError with proper error name for identification - Store last sync error and error time in settings for UI display - Add error status display in autosync settings modal - Show user-friendly error messages with GitHub token suggestion - Clear error status on successful sync - Update both GitHubJsonService and GitHubService with rate limit error handling This provides better user feedback when GitHub API rate limits are exceeded and guides users to set up a GitHub token for higher rate limits.
This commit is contained in:
@@ -46,6 +46,8 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
||||
const [appriseUrls, setAppriseUrls] = useState<string[]>([]);
|
||||
const [appriseUrlsText, setAppriseUrlsText] = useState('');
|
||||
const [lastAutoSync, setLastAutoSync] = useState('');
|
||||
const [lastAutoSyncError, setLastAutoSyncError] = useState<string | null>(null);
|
||||
const [lastAutoSyncErrorTime, setLastAutoSyncErrorTime] = useState<string | null>(null);
|
||||
const [cronValidationError, setCronValidationError] = useState('');
|
||||
|
||||
// Load existing settings when modal opens
|
||||
@@ -311,6 +313,8 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
||||
setAppriseUrls(settings.appriseUrls ?? []);
|
||||
setAppriseUrlsText((settings.appriseUrls ?? []).join('\n'));
|
||||
setLastAutoSync(settings.lastAutoSync ?? '');
|
||||
setLastAutoSyncError(settings.lastAutoSyncError ?? null);
|
||||
setLastAutoSyncErrorTime(settings.lastAutoSyncErrorTime ?? null);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -322,17 +326,6 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
||||
setIsSaving(true);
|
||||
setMessage(null);
|
||||
|
||||
console.log('Saving auto-sync settings:', {
|
||||
autoSyncEnabled,
|
||||
syncIntervalType,
|
||||
syncIntervalPredefined,
|
||||
syncIntervalCron,
|
||||
autoDownloadNew,
|
||||
autoUpdateExisting,
|
||||
notificationEnabled,
|
||||
appriseUrls
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/settings/auto-sync', {
|
||||
method: 'POST',
|
||||
@@ -348,17 +341,12 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
||||
appriseUrls: appriseUrls
|
||||
})
|
||||
});
|
||||
|
||||
console.log('API response status:', response.status);
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
console.log('API response data:', result);
|
||||
setMessage({ type: 'success', text: 'Auto-sync settings saved successfully!' });
|
||||
setTimeout(() => setMessage(null), 3000);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
console.error('API error:', errorData);
|
||||
setMessage({ type: 'error', text: errorData.error ?? 'Failed to save auto-sync settings' });
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -817,7 +805,6 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
||||
<Toggle
|
||||
checked={autoSyncEnabled}
|
||||
onCheckedChange={async (checked) => {
|
||||
console.log('Toggle changed to:', checked);
|
||||
setAutoSyncEnabled(checked);
|
||||
|
||||
// Auto-save when toggle changes
|
||||
@@ -843,14 +830,10 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Auto-sync toggle saved successfully');
|
||||
// Update local state to reflect the effective sync interval type
|
||||
if (effectiveSyncIntervalType !== syncIntervalType) {
|
||||
setSyncIntervalType(effectiveSyncIntervalType);
|
||||
}
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
console.error('Failed to save auto-sync toggle:', errorData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving auto-sync toggle:', error);
|
||||
@@ -1047,6 +1030,25 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lastAutoSyncError && (
|
||||
<div className="p-3 bg-error/10 text-error-foreground border border-error/20 rounded-md">
|
||||
<div className="flex items-start gap-2">
|
||||
<svg className="w-4 h-4 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Last sync error:</p>
|
||||
<p className="text-sm mt-1">{lastAutoSyncError}</p>
|
||||
{lastAutoSyncErrorTime && (
|
||||
<p className="text-xs mt-1 opacity-75">
|
||||
{new Date(lastAutoSyncErrorTime).toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={triggerManualSync}
|
||||
|
||||
@@ -136,7 +136,9 @@ export async function POST(request: NextRequest) {
|
||||
'AUTO_UPDATE_EXISTING': settings.autoUpdateExisting ? 'true' : 'false',
|
||||
'NOTIFICATION_ENABLED': settings.notificationEnabled ? 'true' : 'false',
|
||||
'APPRISE_URLS': Array.isArray(settings.appriseUrls) ? JSON.stringify(settings.appriseUrls) : (settings.appriseUrls || '[]'),
|
||||
'LAST_AUTO_SYNC': settings.lastAutoSync || ''
|
||||
'LAST_AUTO_SYNC': settings.lastAutoSync || '',
|
||||
'LAST_AUTO_SYNC_ERROR': settings.lastAutoSyncError || '',
|
||||
'LAST_AUTO_SYNC_ERROR_TIME': settings.lastAutoSyncErrorTime || ''
|
||||
};
|
||||
|
||||
// Update or add each setting
|
||||
@@ -177,6 +179,9 @@ export async function POST(request: NextRequest) {
|
||||
autoSyncService.stopAutoSync();
|
||||
// Ensure the service is completely stopped and won't restart
|
||||
autoSyncService.isRunning = false;
|
||||
// Also stop the global service instance if it exists
|
||||
const { stopAutoSync: stopGlobalAutoSync } = await import('../../../../server/lib/autoSyncInit.js');
|
||||
stopGlobalAutoSync();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error rescheduling auto-sync service:', error);
|
||||
@@ -212,7 +217,9 @@ export async function GET() {
|
||||
autoUpdateExisting: false,
|
||||
notificationEnabled: false,
|
||||
appriseUrls: [],
|
||||
lastAutoSync: ''
|
||||
lastAutoSync: '',
|
||||
lastAutoSyncError: null,
|
||||
lastAutoSyncErrorTime: null
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -236,7 +243,9 @@ export async function GET() {
|
||||
return [];
|
||||
}
|
||||
})(),
|
||||
lastAutoSync: getEnvValue(envContent, 'LAST_AUTO_SYNC') || ''
|
||||
lastAutoSync: getEnvValue(envContent, 'LAST_AUTO_SYNC') || '',
|
||||
lastAutoSyncError: getEnvValue(envContent, 'LAST_AUTO_SYNC_ERROR') || null,
|
||||
lastAutoSyncErrorTime: getEnvValue(envContent, 'LAST_AUTO_SYNC_ERROR_TIME') || null
|
||||
};
|
||||
|
||||
return NextResponse.json({ settings });
|
||||
|
||||
@@ -48,7 +48,9 @@ export class AutoSyncService {
|
||||
autoUpdateExisting: false,
|
||||
notificationEnabled: false,
|
||||
appriseUrls: [],
|
||||
lastAutoSync: ''
|
||||
lastAutoSync: '',
|
||||
lastAutoSyncError: null,
|
||||
lastAutoSyncErrorTime: null
|
||||
};
|
||||
const lines = envContent.split('\n');
|
||||
|
||||
@@ -93,6 +95,12 @@ export class AutoSyncService {
|
||||
case 'LAST_AUTO_SYNC':
|
||||
settings.lastAutoSync = value;
|
||||
break;
|
||||
case 'LAST_AUTO_SYNC_ERROR':
|
||||
settings.lastAutoSyncError = value;
|
||||
break;
|
||||
case 'LAST_AUTO_SYNC_ERROR_TIME':
|
||||
settings.lastAutoSyncErrorTime = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,7 +117,9 @@ export class AutoSyncService {
|
||||
autoUpdateExisting: false,
|
||||
notificationEnabled: false,
|
||||
appriseUrls: [],
|
||||
lastAutoSync: ''
|
||||
lastAutoSync: '',
|
||||
lastAutoSyncError: null,
|
||||
lastAutoSyncErrorTime: null
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -149,7 +159,9 @@ export class AutoSyncService {
|
||||
'AUTO_UPDATE_EXISTING': settings.autoUpdateExisting.toString(),
|
||||
'NOTIFICATION_ENABLED': settings.notificationEnabled.toString(),
|
||||
'APPRISE_URLS': JSON.stringify(settings.appriseUrls || []),
|
||||
'LAST_AUTO_SYNC': settings.lastAutoSync || ''
|
||||
'LAST_AUTO_SYNC': settings.lastAutoSync || '',
|
||||
'LAST_AUTO_SYNC_ERROR': settings.lastAutoSyncError || '',
|
||||
'LAST_AUTO_SYNC_ERROR_TIME': settings.lastAutoSyncErrorTime || ''
|
||||
};
|
||||
|
||||
const existingKeys = new Set();
|
||||
@@ -426,9 +438,13 @@ export class AutoSyncService {
|
||||
await this.sendSyncNotification(results);
|
||||
}
|
||||
|
||||
// Step 4: Update last sync time
|
||||
// Step 4: Update last sync time and clear any previous errors
|
||||
const lastSyncTime = this.safeToISOString(new Date());
|
||||
const updatedSettings = { ...settings, lastAutoSync: lastSyncTime };
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
lastAutoSync: lastSyncTime,
|
||||
lastAutoSyncError: null // Clear any previous errors on successful sync
|
||||
};
|
||||
this.saveSettings(updatedSettings);
|
||||
|
||||
const duration = new Date().getTime() - startTime.getTime();
|
||||
@@ -444,13 +460,22 @@ export class AutoSyncService {
|
||||
} catch (error) {
|
||||
console.error('Auto-sync execution failed:', error);
|
||||
|
||||
// Check if it's a rate limit error
|
||||
const isRateLimitError = error instanceof Error && error.name === 'RateLimitError';
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// Send error notification if enabled
|
||||
const settings = this.loadSettings();
|
||||
if (settings.notificationEnabled && settings.appriseUrls?.length > 0) {
|
||||
try {
|
||||
const notificationTitle = isRateLimitError ? 'Auto-Sync Rate Limited' : 'Auto-Sync Failed';
|
||||
const notificationMessage = isRateLimitError
|
||||
? `GitHub API rate limit exceeded. Please set a GITHUB_TOKEN in your .env file for higher rate limits. Error: ${errorMessage}`
|
||||
: `Auto-sync failed with error: ${errorMessage}`;
|
||||
|
||||
await appriseService.sendNotification(
|
||||
'Auto-Sync Failed',
|
||||
`Auto-sync failed with error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
notificationTitle,
|
||||
notificationMessage,
|
||||
settings.appriseUrls
|
||||
);
|
||||
} catch (notifError) {
|
||||
@@ -458,10 +483,24 @@ export class AutoSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
// Store the error in settings for UI display
|
||||
const errorSettings = this.loadSettings();
|
||||
const errorToStore = isRateLimitError
|
||||
? `GitHub API rate limit exceeded. Please set a GITHUB_TOKEN in your .env file for higher rate limits.`
|
||||
: errorMessage;
|
||||
|
||||
const updatedErrorSettings = {
|
||||
...errorSettings,
|
||||
lastAutoSyncError: errorToStore,
|
||||
lastAutoSyncErrorTime: this.safeToISOString(new Date())
|
||||
};
|
||||
this.saveSettings(updatedErrorSettings);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
message: errorToStore,
|
||||
error: errorMessage,
|
||||
isRateLimitError
|
||||
};
|
||||
} finally {
|
||||
this.isRunning = false;
|
||||
|
||||
@@ -37,16 +37,15 @@ export class GitHubService {
|
||||
// Add GitHub token authentication if available
|
||||
if (env.GITHUB_TOKEN) {
|
||||
headers.Authorization = `token ${env.GITHUB_TOKEN}`;
|
||||
console.log('Using GitHub token for API authentication');
|
||||
} else {
|
||||
console.log('No GitHub token found, using unauthenticated requests (lower rate limits)');
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}${endpoint}`, { headers });
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
throw new Error(`GitHub API rate limit exceeded. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
|
||||
const error = new Error(`GitHub API rate limit exceeded. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
|
||||
error.name = 'RateLimitError';
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
@@ -50,16 +50,15 @@ export class GitHubJsonService {
|
||||
// Add GitHub token authentication if available
|
||||
if (env.GITHUB_TOKEN) {
|
||||
headers.Authorization = `token ${env.GITHUB_TOKEN}`;
|
||||
console.log('Using GitHub token for API authentication');
|
||||
} else {
|
||||
console.log('No GitHub token found, using unauthenticated requests (lower rate limits)');
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl!}${endpoint}`, { headers });
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
throw new Error(`GitHub API rate limit exceeded. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
|
||||
const error = new Error(`GitHub API rate limit exceeded. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
|
||||
error.name = 'RateLimitError';
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
@@ -83,7 +82,9 @@ export class GitHubJsonService {
|
||||
const response = await fetch(rawUrl, { headers });
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
throw new Error(`GitHub rate limit exceeded while downloading ${filePath}. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
|
||||
const error = new Error(`GitHub rate limit exceeded while downloading ${filePath}. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
|
||||
error.name = 'RateLimitError';
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user