{"record":{"id":"c5d5fd739be049ca","repo":"infiniflow/ragflow","slug":"github-search-returned-no-items","errorCode":null,"errorMessage":"GitHub search returned no items.","messagePattern":"GitHub search returned no items\\.","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"agent/tools/github.py","lineNumber":83,"sourceCode":"\n        last_e = \"\"\n        for _ in range(self._param.max_retries + 1):\n            if self.check_if_canceled(\"GitHub processing\"):\n                return\n\n            try:\n                url = \"https://api.github.com/search/repositories?q=\" + kwargs[\"query\"] + \"&sort=stars&order=desc&per_page=\" + str(self._param.top_n)\n                headers = {\"Content-Type\": \"application/vnd.github+json\", \"X-GitHub-Api-Version\": \"2022-11-28\"}\n                response = requests.get(url=url, headers=headers, timeout=DEFAULT_TIMEOUT).json()\n\n                if self.check_if_canceled(\"GitHub processing\"):\n                    return\n\n                # the github search api reports rate limits (403/429) and invalid\n                # queries (422) through a \"message\" field and omits \"items\"; surface\n                # that instead of raising a cryptic KeyError on the missing key.\n                if \"items\" not in response:\n                    raise Exception(response.get(\"message\", \"GitHub search returned no items.\"))\n\n                items = response[\"items\"]\n                self._retrieve_chunks(items, get_title=lambda r: r[\"name\"], get_url=lambda r: r[\"html_url\"], get_content=lambda r: str(r[\"description\"]) + \"\\n stars:\" + str(r[\"watchers\"]))\n                self.set_output(\"json\", items)\n                return self.output(\"formalized_content\")\n            except Exception as e:\n                if self.check_if_canceled(\"GitHub processing\"):\n                    return\n\n                last_e = e\n                logging.exception(f\"GitHub error: {e}\")\n                time.sleep(self._param.delay_after_error)\n\n        if last_e:\n            self.set_output(\"_ERROR\", str(last_e))\n            return f\"GitHub error: {last_e}\"\n\n        assert False, self.output()","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/infiniflow/ragflow/blob/554fb1133ac3861732235ad9c377eb5e0a770665/agent/tools/github.py#L65-L101","documentation":"Raised by the GitHub agent tool when the GitHub Search API response JSON has no 'items' key. GitHub reports rate limits (403/429) and invalid queries (422) through a top-level 'message' field while omitting 'items', so the tool surfaces that message; the literal 'GitHub search returned no items.' only appears when the response has neither key. It replaces what used to be a cryptic KeyError on response['items'].","triggerScenarios":"GET https://api.github.com/search/repositories?q=<query>&sort=stars&order=desc&per_page=N returning 403 'rate limit exceeded' (60 req/hr unauthenticated), 422 'Validation Failed' for queries with bad qualifiers (e.g. 'language:' with empty value or stray quotes), or 401 for a revoked token if auth headers are added.","commonSituations":"Agent workflows that call GitHub search in a loop without a GITHUB_TOKEN, CI runs sharing an office IP that exhausted the anonymous quota, LLM-generated queries containing unescaped quotes or invalid qualifiers like 'repo:' in a repository search, or GitHub secondary rate limiting during bursts.","solutions":["Check the exception text: 'rate limit exceeded' means wait for the X-RateLimit-Reset window or authenticate the request to raise the limit to 5000/hr (the current code sends no Authorization header).","If it says 'Validation Failed', inspect the query string — remove empty qualifiers, unmatched quotes, or invalid sort/filter syntax.","Add exponential backoff honoring Retry-After on 403/429 instead of the fixed delay_after_error sleep.","Cache results for repeated queries to stay under the 60/hr anonymous quota."],"exampleFix":"// before\nheaders = {\"Content-Type\": \"application/vnd.github+json\", \"X-GitHub-Api-Version\": \"2022-11-28\"}\nresponse = requests.get(url=url, headers=headers, timeout=DEFAULT_TIMEOUT).json()\n\n// after\ntoken = os.environ.get(\"GITHUB_TOKEN\")\nheaders = {\"Content-Type\": \"application/vnd.github+json\", \"X-GitHub-Api-Version\": \"2022-11-28\"}\nif token:\n    headers[\"Authorization\"] = f\"Bearer {token}\"\nresp = requests.get(url=url, headers=headers, timeout=DEFAULT_TIMEOUT)\nif resp.status_code in (403, 429):\n    time.sleep(int(resp.headers.get(\"Retry-After\", \"30\")))\nresponse = resp.json()","handlingStrategy":"try-catch","validationCode":"def safe_github_query(q: str) -> str:\n    import re\n    q = q.strip().strip('\"\\'')\n    if not q:\n        raise ValueError(\"empty query\")\n    return q","typeGuard":null,"tryCatchPattern":"try:\n    out = github_tool._invoke(query=q)\nexcept Exception as e:\n    if \"rate limit\" in str(e).lower():\n        wait_for_reset(); rerun()\n    elif \"items\" in str(e) or \"no items\" in str(e):\n        return []  # empty result is not fatal for search flows\n    raise","preventionTips":["Authenticate GitHub API calls with a token to lift the 60/hr anonymous limit to 5000/hr.","Honor Retry-After / X-RateLimit-Reset instead of fixed sleeps.","Sanitize LLM-generated queries (strip quotes, drop empty qualifiers) before they reach the API."],"tags":["api","github","rate-limit","agent-tool","http"],"backgroundTag":null,"analyzedSha":"554fb1133ac3861732235ad9c377eb5e0a770665","analyzedAt":"2026-08-15T09:20:16.380Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}