{"record":{"id":"e8b2c5235b49039d","repo":"infiniflow/ragflow","slug":"failed-to-fetch-courses-e","errorCode":null,"errorMessage":"Failed to fetch courses: {e}","messagePattern":"Failed to fetch courses: (.+?)","errorType":"exception","errorClass":"ConnectorValidationError","httpStatus":null,"severity":"error","filePath":"common/data_source/moodle_connector.py","lineNumber":210,"sourceCode":"                    f\"slim snapshot for course {getattr(course, 'fullname', '?')}\",\n                    e,\n                )\n\n        if batch:\n            yield batch\n\n        logger.info(f\"Moodle slim snapshot completed: {total} documents listed\")\n\n    @retry(tries=3, delay=1, backoff=2)\n    def _get_enrolled_courses(self) -> list:\n        if not self.moodle_client:\n            raise ConnectorMissingCredentialError(\"Moodle client not initialized\")\n\n        try:\n            return self.moodle_client.core.course.get_courses()\n        except MoodleException as e:\n            self._log_error(\"fetching courses\", e, \"error\")\n            raise ConnectorValidationError(f\"Failed to fetch courses: {e}\")\n\n    @retry(tries=3, delay=1, backoff=2)\n    def _get_course_contents(self, course_id: int):\n        if not self.moodle_client:\n            raise ConnectorMissingCredentialError(\"Moodle client not initialized\")\n\n        try:\n            return self.moodle_client.core.course.get_contents(courseid=course_id)\n        except MoodleException as e:\n            self._log_error(f\"fetching course contents for {course_id}\", e)\n            return []\n\n    def _process_courses(self, courses) -> Generator[Document, None, None]:\n        for course in courses:\n            try:\n                contents = self._get_course_contents(course.id)\n                for section in contents:\n                    for module in section.modules:","sourceCodeStart":192,"sourceCodeEnd":228,"githubUrl":"https://github.com/infiniflow/ragflow/blob/554fb1133ac3861732235ad9c377eb5e0a770665/common/data_source/moodle_connector.py#L192-L228","documentation":"A ConnectorValidationError raised in _get_enrolled_courses when core.course.get_courses() throws a MoodleException. The raw Moodle error text is embedded, and because the helper is wrapped in @retry(tries=3, delay=1, backoff=2) Moodle-side errors are retried three times before this surfaces. Unlike its sibling _get_course_contents (which swallows errors and returns []), the course fetch is treated as fatal — without courses there is nothing to index.","triggerScenarios":"Calling any load path when Moodle rejects core_course_get_courses — token's service lacks that function, user lacks capability to view courses, or Moodle returns an error like 'accessexception'/'invalidtoken' at course-list time.","commonSituations":"Token created on a custom service that did not include core_course_get_courses; user role can authenticate but not list courses; Moodle upgrade that renamed/removed the function; debugging confusion from the triple retry in logs.","solutions":["Read the embedded {e} text — Moodle error codes (invalidtoken, accessexception, missing capability, invalidfunction) each point to a distinct fix.","Ensure the token's service includes core_course_get_courses (and core_course_get_contents for later steps), or use an admin-issued token.","Grant the token user the moodle/course:view capability on the courses to index.","Verify directly: curl '<moodle_url>/webservice/rest/server.php?wstoken=TOKEN&wsfunction=core_course_get_courses&moodlewsrestformat=json'."],"exampleFix":"# before\nconnector.load_credentials(creds)\nfor batch in connector.load_from_state():  # ConnectorValidationError('Failed to fetch courses: ...')\n    ...\n\n# after\n# fix token service scope in Moodle (include core_course_get_courses), then\nconnector.load_credentials(new_creds)\nimport requests\nr = requests.get(f\"{moodle_url}/webservice/rest/server.php\", params={\"wstoken\": token, \"wsfunction\": \"core_course_get_courses\", \"moodlewsrestformat\": \"json\"})\nr.raise_for_status()  # confirm no exception block in response before re-running the load","handlingStrategy":"retry","validationCode":"import requests\n\ndef can_fetch_courses(moodle_url: str, token: str) -> bool:\n    r = requests.get(\n        f\"{moodle_url}/webservice/rest/server.php\",\n        params={\"wstoken\": token, \"wsfunction\": \"core_course_get_courses\", \"moodlewsrestformat\": \"json\"},\n        timeout=10,\n    )\n    data = r.json()\n    return \"exception\" not in data","typeGuard":null,"tryCatchPattern":"try:\n    for batch in connector.load_from_state():\n        process(batch)\nexcept ConnectorValidationError as exc:\n    if not str(exc).startswith(\"Failed to fetch courses\"):\n        raise\n    msg = str(exc).lower()\n    if \"invalidtoken\" in msg:\n        refresh_moodle_credentials(connector)\n    elif \"accessexception\" in msg or \"capability\" in msg:\n        notify_admin_to_grant_course_access()\n    else:\n        raise","preventionTips":["Create the Moodle token on a service that includes core_course_get_courses and core_course_get_contents.","Pre-flight the course-list call once before starting a full load.","Distinguish this fatal course-fetch error from the sibling _get_course_contents, which silently returns [] — check logs accordingly."],"tags":["moodle","permissions","web-services","retry","courses"],"backgroundTag":null,"analyzedSha":"554fb1133ac3861732235ad9c377eb5e0a770665","analyzedAt":"2026-08-15T09:20:16.380Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}