{"record":{"id":"6a5f910a5623bdd4","repo":"infiniflow/ragflow","slug":"airtable-credentials-not-loaded","errorCode":null,"errorMessage":"Airtable credentials not loaded","messagePattern":"Airtable credentials not loaded","errorType":"validation","errorClass":"ConnectorMissingCredentialError","httpStatus":null,"severity":"error","filePath":"common/data_source/airtable_connector.py","lineNumber":49,"sourceCode":"    This connector ingests Airtable attachments as raw blobs without\n    parsing file content or generating text/image sections.\n    \"\"\"\n\n    def __init__(\n        self,\n        base_id: str,\n        table_name_or_id: str,\n        batch_size: int = INDEX_BATCH_SIZE,\n    ) -> None:\n        self.base_id = base_id\n        self.table_name_or_id = table_name_or_id\n        self.batch_size = batch_size\n        self._airtable_client: AirtableApi | None = None\n        self.size_threshold = AIRTABLE_CONNECTOR_SIZE_THRESHOLD\n\n    def _iter_attachment_entries(self) -> Generator[tuple[str, str, str, str, str | None, dict[str, Any]], None, None]:\n        if not self._airtable_client:\n            raise ConnectorMissingCredentialError(\"Airtable credentials not loaded\")\n\n        table = self.airtable_client.table(self.base_id, self.table_name_or_id)\n        records = table.all()\n\n        logging.info(f\"Starting Airtable attachment scan for table {self.table_name_or_id}, {len(records)} records found.\")\n\n        for record in records:\n            record_id = record.get(\"id\")\n            fields = record.get(\"fields\", {})\n            created_time = record.get(\"createdTime\")\n\n            for field_value in fields.values():\n                if not isinstance(field_value, list):\n                    continue\n\n                for attachment in field_value:\n                    filename = attachment.get(\"filename\")\n                    attachment_id = attachment.get(\"id\")","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/infiniflow/ragflow/blob/554fb1133ac3861732235ad9c377eb5e0a770665/common/data_source/airtable_connector.py#L31-L67","documentation":"ConnectorMissingCredentialError raised by AirtableConnector._iter_attachment_entries (common/data_source/airtable_connector.py:56-58). The connector stores its AirtableApi client in self._airtable_client, populated only when credentials are loaded; the attachment-scan generator guards on this and refuses to iterate when no client was ever set, rather than crashing later with an opaque AttributeError. The sister method load_from_state (line ~104) raises the identical error for the same reason.","triggerScenarios":"Calling load_from_state (or anything that drives _iter_attachment_entries) on an AirtableConnector that was constructed with only base_id/table_name_or_id but never had credentials applied (the credential-loading path that sets _airtable_client was skipped or failed silently).","commonSituations":"Building the connector directly in tests/scripts without the credential-loading step the production ingestion service performs; a data-source sync job whose credential fetch returned empty and the code proceeded to load anyway; connector reused after a credential reset.","solutions":["Load/apply Airtable credentials (API key/personal access token) through the same code path the ingestion service uses before calling load_from_state.","Check self._airtable_client (or the public airtable_client property) is non-None before starting the sync and surface a clear configuration error.","If credentials come from the data-source record, verify that record actually contains the airtable token before scheduling the connector."],"exampleFix":"# before\nconn = AirtableConnector(base_id, table)\ndocs = conn.load_from_state()  # raises\n# after\nconn = AirtableConnector(base_id, table)\nconn.load_credentials(credentials)  # sets _airtable_client\ndocs = conn.load_from_state()","handlingStrategy":"validation","validationCode":"if connector._airtable_client is None:\n    raise RuntimeError(\"Airtable credentials not loaded — call the credential loader before scanning attachments\")","typeGuard":"def airtable_ready(connector) -> bool:\n    return connector._airtable_client is not None","tryCatchPattern":"from common.data_source.airtable_connector import ConnectorMissingCredentialError\ntry:\n    entries = list(connector._iter_attachment_entries())\nexcept ConnectorMissingCredentialError:\n    log.error(\"data source missing Airtable token; mark sync failed and notify\")\n    raise","preventionTips":["Always run the connector's credential-loading step right after construction.","Validate the data-source record contains an Airtable token before scheduling sync.","In tests, inject a stub AirtableApi client into _airtable_client."],"tags":["connectors","airtable","credentials","ingestion"],"backgroundTag":null,"analyzedSha":"554fb1133ac3861732235ad9c377eb5e0a770665","analyzedAt":"2026-08-15T09:20:16.380Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}