{"record":{"id":"bff352ecffcd8b24","repo":"infiniflow/ragflow","slug":"api-rate-limit-exceeded-limit-requests-hour-fo","errorCode":null,"errorMessage":"API rate limit exceeded. ${limit} requests/hour for unauthenticated requests.","messagePattern":"API rate limit exceeded\\. (.+?) requests/hour for unauthenticated requests\\.","errorType":"http","errorClass":"Error","httpStatus":403,"severity":"error","filePath":"web/src/pages/skills/components/upload-modal.tsx","lineNumber":383,"sourceCode":"      let url: string;\n      if (platform === 'github') {\n        url = `${config.apiBase}/repos/${owner}/${repo}/contents/${path}?ref=${ref}`;\n      } else {\n        url = `${config.apiBase}/repos/${owner}/${repo}/contents/${path}?ref=${ref}`;\n        if (token) {\n          url += `&access_token=${token}`;\n        }\n      }\n\n      const response = await fetch(url, { headers });\n\n      if (!response.ok) {\n        const errorData = await response.json().catch(() => ({}));\n        const message = errorData.message || `HTTP ${response.status}`;\n\n        if (response.status === 403) {\n          const limit = platform === 'github' ? '60' : '1000';\n          throw new Error(\n            `API rate limit exceeded. ${limit} requests/hour for unauthenticated requests.`,\n          );\n        }\n        if (response.status === 404) {\n          throw new Error(\n            'Repository or path not found. Please check the URL and ensure the repository is public.',\n          );\n        }\n        throw new Error(`Failed to fetch: ${message}`);\n      }\n\n      const items = await response.json();\n      const files: GitFile[] = [];\n\n      // Handle single file case\n      if (!Array.isArray(items)) {\n        if (items.type === 'file') {\n          files.push({","sourceCodeStart":365,"sourceCodeEnd":401,"githubUrl":"https://github.com/infiniflow/ragflow/blob/554fb1133ac3861732235ad9c377eb5e0a770665/web/src/pages/skills/components/upload-modal.tsx#L365-L401","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nif (response.status === 403) {\n  const limit = platform === 'github' ? '60' : '1000';\n  throw new Error(`API rate limit exceeded. ${limit} requests/hour for unauthenticated requests.`);\n}\n\n// after\nif (response.status === 403) {\n  const remaining = response.headers.get('x-ratelimit-remaining');\n  if (remaining === '0') {\n    throw new Error(`API rate limit exceeded. Add a personal access token to raise the limit.`);\n  }\n  throw new Error('Access denied. If this is a private repository, provide a token with read access.');\n}","handlingStrategy":"retry","validationCode":"// Check rate-limit headers before the heavy import\nconst probe = await fetch(`https://api.${platform}.com/rate_limit`, {\n  headers: token ? { Authorization: `Bearer ${token}` } : undefined,\n});\nconst remaining = Number(probe.headers.get('x-ratelimit-remaining') ?? '0');\nif (remaining < filesToFetch) {\n  showWarning('Rate limit low — add a token or retry later');\n}","typeGuard":"function isRateLimitResponse(res: Response): boolean {\n  return res.status === 403 && res.headers.get('x-ratelimit-remaining') === '0';\n}","tryCatchPattern":"try {\n  const files = await fetchGitDirectoryContents(platform, owner, repo, path, ref, token);\n} catch (e) {\n  const msg = e instanceof Error ? e.message : String(e);\n  if (msg.includes('rate limit')) {\n    // back off and retry with backoff, or prompt for a token\n    await promptForTokenAndRetry();\n  } else {\n    throw e;\n  }\n}","preventionTips":["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"],"tags":["rate-limit","github-api","gitee","network","skills"],"backgroundTag":null,"analyzedSha":"554fb1133ac3861732235ad9c377eb5e0a770665","analyzedAt":"2026-08-15T09:20:16.380Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}