Skip to content

Commit fb36e48

Browse files
author
Irving Popovetsky
committed
Fix AirtableAPI session initialization race condition
The AirtableAPI was being initialized in plugin.load() before the aiohttp session existed, causing 'self.session' to be None. This led to production errors during /mentor command: "AttributeError: 'NoneType' object has no attribute 'get'". Solution: - Defer AirtableAPI initialization to startup callback (same pattern as SlackPlugin) - Make _initialize_api() idempotent to preserve test mocks - Update test fixtures to manually trigger initialization when needed
1 parent 4b2d484 commit fb36e48

3 files changed

Lines changed: 33 additions & 4 deletions

File tree

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,22 @@ Similarly, the `/mentor` and `/mentor-volunteer` commands require access to an A
262262
environment with a specific configuration. If you're planning on working with the mentor
263263
functionality please reach out to the `#oc-python-projects` channel for help getting set up.
264264

265+
### Airtable Authentication
266+
267+
**⚠️ IMPORTANT:** Airtable deprecated API keys on February 1, 2024. You must use a **Personal Access Token (PAT)**:
268+
269+
1. Go to [Airtable Developer Hub](https://airtable.com/create/tokens)
270+
2. Click **Create new token**
271+
3. Configure the token with required scopes:
272+
- `data.records:read` - Read records from tables
273+
- `data.records:write` - Create/update records
274+
- `schema.bases:read` - Read base schema
275+
4. Select the specific base(s) to grant access to
276+
5. Copy the generated token (starts with `pat...`)
277+
6. Set environment variable: `AIRTABLE_API_KEY=<your-pat-token>`
278+
279+
**Note:** Despite the variable name `AIRTABLE_API_KEY`, you must use a Personal Access Token, not the deprecated API key format.
280+
265281
#### Interactive Components
266282

267283
You can follow the instructions (and read helpful related information) on the
@@ -290,6 +306,9 @@ Name | Description | Example
290306
---- | ----------- | -------
291307
SLACK_BOT_SIGNING_SECRET | The unique signing secret used by Slack for a specific app that will be validated by pybot when inspecting an inbound API request | f3b4d774b79e0fb55af624c3f376d5b4
292308
BOT_USER_OAUTH_ACCESS_TOKEN | The bot user specific OAuth token used to authenticate the bot when making API requests to Slack | xoxb-800506043194-810119867738-vRvgSc3rslDUgQakFbMy3wAt
309+
AIRTABLE_API_KEY | Airtable Personal Access Token (PAT) for API authentication. **Must be a PAT (starts with `pat...`), not deprecated API key** | patAbCd1234567890.1234567890abcdefghijklmnop
310+
AIRTABLE_BASE_KEY | The base ID for your Airtable base | appSqQz7spgg0I1jQ
311+
YELP_TOKEN | API token for Yelp Fusion API (required for `/lunch` command) | your-yelp-api-token
293312

294313
## License
295314
This package is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).

pybot/plugins/airtable/plugin.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,24 @@ def load(
4242
base_key: str | None = None,
4343
verify: str | None = None,
4444
) -> None:
45-
self.session = sirbot.http_session
45+
# Store reference to sirbot for later initialization
46+
self._sirbot = sirbot
4647
self.api_key = api_key or os.environ.get("AIRTABLE_API_KEY", "")
4748
self.base_key = base_key or os.environ.get("AIRTABLE_BASE_KEY", "")
4849
self.verify = verify or os.environ.get("AIRTABLE_VERIFY", "")
4950

50-
self.api = AirtableAPI(self.session, self.api_key, self.base_key)
51+
# Initialize API after session is created
52+
sirbot.on_startup.append(self._initialize_api)
5153

5254
sirbot.router.add_route("POST", "/airtable/request", endpoints.incoming_request)
5355

56+
async def _initialize_api(self, app: Any) -> None:
57+
"""Initialize AirtableAPI after http_session is created."""
58+
if self.api is None:
59+
logger.info("Initializing Airtable API client")
60+
self.session = self._sirbot.http_session
61+
self.api = AirtableAPI(self.session, self.api_key, self.base_key)
62+
5463
def on_request(self, request: str, handler: AsyncHandler, **kwargs: Any) -> None:
5564
handler = _ensure_async(handler)
5665
options = {**kwargs, "wait": False}

tests/conftest.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ async def bot() -> SirBot:
4444
b.load_plugin(airtable)
4545
b.load_plugin(api)
4646

47-
# Manually initialize the Slack API for tests that don't use aiohttp_client
47+
# Manually initialize API clients for tests that don't use aiohttp_client
4848
# (which would trigger startup callbacks automatically).
49-
# The _initialize_api method is idempotent, so it won't overwrite mocks.
49+
# The _initialize_api methods are idempotent, so they won't overwrite mocks.
5050
await slack._initialize_api(b)
51+
await airtable._initialize_api(b)
5152

5253
yield b
5354

0 commit comments

Comments
 (0)