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

  1. Enter a personal access token in the token field of the upload modal so requests authenticate (GitHub limit rises to 5000/hour)
  2. Wait for the rate-limit window to reset (GitHub resets hourly; check X-RateLimit-Reset header via curl)
  3. If the repo is private, use a token with repo scope — a 403 here is not always rate limiting
  4. 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

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

Related errors


AI-assisted analysis of infiniflow/ragflow@554fb1133a (2026-08-15). Data as JSON: /api/errors/bff352ecffcd8b24. Report an issue: GitHub.