infiniflow/ragflow · error · Error
API rate limit exceeded. ${limit} requests/hour for unauthen
Error message
API rate limit exceeded. ${limit} requests/hour for unauthenticated requests. What it means
Raised by the skill upload modal's GitHub/Gitee importer when the git contents API responds with HTTP 403. For unauthenticated requests GitHub allows only 60 requests/hour per IP (Gitee ~1000), so a 403 almost always means the rate limit is exhausted. The message hardcodes the limit based on the detected platform.
Source
Thrown at web/src/pages/skills/components/upload-modal.tsx:383
let url: string;
if (platform === 'github') {
url = `${config.apiBase}/repos/${owner}/${repo}/contents/${path}?ref=${ref}`;
} else {
url = `${config.apiBase}/repos/${owner}/${repo}/contents/${path}?ref=${ref}`;
if (token) {
url += `&access_token=${token}`;
}
}
const response = await fetch(url, { headers });
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const message = errorData.message || `HTTP ${response.status}`;
if (response.status === 403) {
const limit = platform === 'github' ? '60' : '1000';
throw new Error(
`API rate limit exceeded. ${limit} requests/hour for unauthenticated requests.`,
);
}
if (response.status === 404) {
throw new Error(
'Repository or path not found. Please check the URL and ensure the repository is public.',
);
}
throw new Error(`Failed to fetch: ${message}`);
}
const items = await response.json();
const files: GitFile[] = [];
// Handle single file case
if (!Array.isArray(items)) {
if (items.type === 'file') {
files.push({View on GitHub (pinned to 554fb1133a)
Solutions
- Enter a personal access token in the token field of the upload modal so requests authenticate (GitHub limit rises to 5000/hour)
- Wait for the rate-limit window to reset (GitHub resets hourly; check X-RateLimit-Reset header via curl)
- If the repo is private, use a token with repo scope — a 403 here is not always rate limiting
- Retry from a different network/IP if sharing a constrained NAT
Example fix
// before
if (response.status === 403) {
const limit = platform === 'github' ? '60' : '1000';
throw new Error(`API rate limit exceeded. ${limit} requests/hour for unauthenticated requests.`);
}
// after
if (response.status === 403) {
const remaining = response.headers.get('x-ratelimit-remaining');
if (remaining === '0') {
throw new Error(`API rate limit exceeded. Add a personal access token to raise the limit.`);
}
throw new Error('Access denied. If this is a private repository, provide a token with read access.');
} Defensive patterns
Strategy: retry
Validate before calling
// Check rate-limit headers before the heavy import
const probe = await fetch(`https://api.${platform}.com/rate_limit`, {
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
});
const remaining = Number(probe.headers.get('x-ratelimit-remaining') ?? '0');
if (remaining < filesToFetch) {
showWarning('Rate limit low — add a token or retry later');
} Type guard
function isRateLimitResponse(res: Response): boolean {
return res.status === 403 && res.headers.get('x-ratelimit-remaining') === '0';
} Try / catch
try {
const files = await fetchGitDirectoryContents(platform, owner, repo, path, ref, token);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes('rate limit')) {
// back off and retry with backoff, or prompt for a token
await promptForTokenAndRetry();
} else {
throw e;
}
} Prevention
- Always supply a personal access token for GitHub imports (60/h unauthenticated vs 5000/h authenticated)
- Cache directory listings during a session instead of refetching on every modal open
- Check X-RateLimit-Remaining headers before batch operations
When it happens
Trigger: POST/GET to api.github.com/repos/{owner}/{repo}/contents/{path} (or gitee.com equivalent) without a token returns 403 after 60 (GitHub) or 1000 (Gitee) requests/hour from the same IP; sharing an office NAT/IP multiplies hit rate; no gitToken was supplied so the request went out unauthenticated.
Common situations: Repeatedly testing the import dialog during development; CI/shared IP environments; Gitee also returns 403 for private repos, which is misdiagnosed as a rate limit by this handler.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Repository or path not found. Please check the URL and ensur
- Failed to fetch: ${message}
- Failed to download ${file.path}: ${response.status} ${respon
- No files could be downloaded. Errors:\n${downloadErrors.slic
- Failed to fetch skills
AI-assisted analysis of infiniflow/ragflow@554fb1133a (2026-08-15).
Data as JSON: /api/errors/bff352ecffcd8b24.
Report an issue: GitHub.