Skip to content

Commit cfb227f

Browse files
author
Irving Popovetsky
authored
Merge pull request #418 from OperationCode/irving/fix_issues_post_python_upgrade
Fix Airtable API session initialization race condition
2 parents 7759198 + 1011777 commit cfb227f

File tree

6 files changed

+459
-6
lines changed

6 files changed

+459
-6
lines changed

CLAUDE_CODE_REVIEW.md

Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
# Code Review: Potential Crash & Stacktrace Issues
2+
3+
**Review Date:** 2026-01-05
4+
**Reviewer:** Claude Code
5+
**Codebase:** operationcode-pybot
6+
7+
---
8+
9+
## Executive Summary
10+
11+
This review identified **15 issues** across the codebase that could lead to crashes, unhandled exceptions, or stacktraces in production. Issues are categorized by severity and grouped into a phased remediation plan.
12+
13+
---
14+
15+
## Issues Identified
16+
17+
### 1. Empty Businesses Array Crash in Lunch Command
18+
19+
**Severity:** High
20+
**Location:** `pybot/endpoints/slack/utils/slash_lunch.py:32-34`
21+
22+
```python
23+
def select_random_lunch(self, lunch_response: dict) -> dict:
24+
location_count = len(lunch_response["businesses"])
25+
selected_location = randint(0, location_count - 1) # Crashes if businesses is empty!
26+
```
27+
28+
**Problem:** If Yelp returns zero businesses (no restaurants found), `randint(0, -1)` raises `ValueError: empty range for randrange()`.
29+
30+
**Fix:** Add guard clause to check for empty results before selection.
31+
32+
---
33+
34+
### 2. Unhandled Yelp API Errors
35+
36+
**Severity:** High
37+
**Location:** `pybot/endpoints/slack/commands.py:95-97`
38+
39+
```python
40+
async with app.http_session.get(**request) as r:
41+
r.raise_for_status() # Raises on 4xx/5xx with no try/except
42+
message_params = lunch.select_random_lunch(await r.json())
43+
```
44+
45+
**Problem:** HTTP errors from Yelp API (rate limiting, auth failures, network issues) will crash the handler. The `catch_command_slack_error` decorator only catches `SlackAPIError`, not `aiohttp.ClientError`.
46+
47+
**Fix:** Wrap in try/except to handle `aiohttp.ClientResponseError` and `aiohttp.ClientError`.
48+
49+
---
50+
51+
### 3. Missing Email KeyError in Multiple Locations
52+
53+
**Severity:** High
54+
**Locations:**
55+
- `pybot/endpoints/slack/utils/event_utils.py:88-89`
56+
- `pybot/endpoints/slack/actions/mentor_request.py:27-28`
57+
- `pybot/endpoints/slack/actions/mentor_request.py:129-130`
58+
59+
```python
60+
user_info = await slack_api.query(methods.USERS_INFO, {"user": slack_id})
61+
email = user_info["user"]["profile"]["email"] # KeyError if user has no email
62+
```
63+
64+
**Problem:** Not all Slack users have an email in their profile (guests, restricted accounts, privacy settings). Accessing this without a check raises `KeyError`.
65+
66+
**Fix:** Use `.get()` with fallback or wrap in try/except.
67+
68+
---
69+
70+
### 4. Empty Service Records IndexError
71+
72+
**Severity:** High
73+
**Location:** `pybot/endpoints/slack/message_templates/mentor_request.py:99-100`
74+
75+
```python
76+
service_records = await airtable.find_records("Services", "Name", self.service)
77+
params["Service"] = [service_records[0]["id"]] # IndexError if no records found!
78+
```
79+
80+
**Problem:** If no matching service is found in Airtable, accessing `[0]` raises `IndexError`.
81+
82+
**Fix:** Add guard clause to verify records exist before accessing.
83+
84+
---
85+
86+
### 5. Airtable API Response Key Errors
87+
88+
**Severity:** High
89+
**Location:** `pybot/plugins/airtable/api.py:58-62`
90+
91+
```python
92+
res_json = await self.get(url, params=params)
93+
records = res_json["records"] # KeyError if API returns error response
94+
self.record_id_to_name[table_name] = {
95+
record["id"]: record["fields"]["Name"] for record in records
96+
}
97+
```
98+
99+
**Problem:** Airtable API error responses don't include `records` key. Also assumes all records have a `Name` field.
100+
101+
**Fix:** Add error response checking before accessing `records`.
102+
103+
---
104+
105+
### 6. Empty Mentor Dict KeyError
106+
107+
**Severity:** High
108+
**Location:** `pybot/endpoints/airtable/utils.py:18-21`
109+
110+
```python
111+
mentor = await airtable.get_row_from_record_id("Mentors", requested_mentor)
112+
email = mentor["Email"] # mentor could be {} from exception handler
113+
```
114+
115+
**Problem:** `get_row_from_record_id` returns `{}` on exception, causing `KeyError` when accessing `Email`.
116+
117+
**Fix:** Check if mentor dict is populated before accessing keys.
118+
119+
---
120+
121+
### 7. No Error Handling in Mentor Request Flow
122+
123+
**Severity:** Medium
124+
**Location:** `pybot/endpoints/airtable/requests.py:20-45`
125+
126+
```python
127+
async def mentor_request(request: dict, app: SirBot) -> None:
128+
slack_id = await _slack_user_id_from_email(...)
129+
futures = [...]
130+
service_translation, requested_mentor_message, mentors = await asyncio.gather(*futures)
131+
# If any gather call fails, entire handler crashes
132+
```
133+
134+
**Problem:** Multiple async operations with no error handling. Any failure crashes the entire handler.
135+
136+
**Fix:** Wrap in try/except or use `asyncio.gather(return_exceptions=True)`.
137+
138+
---
139+
140+
### 8. Incorrect Exception Logging Pattern
141+
142+
**Severity:** Medium
143+
**Locations:**
144+
- `pybot/plugins/airtable/api.py:70`
145+
- `pybot/plugins/airtable/api.py:108`
146+
- `pybot/plugins/airtable/api.py:125`
147+
- `pybot/endpoints/slack/actions/mentor_request.py:144`
148+
149+
```python
150+
logger.exception(f"Couldn't get row from record id {record_id}", ex) # Wrong
151+
```
152+
153+
**Problem:** Passing exception as second argument treats it as a format argument. The exception traceback is not logged.
154+
155+
**Fix:** Remove the exception argument; `logger.exception()` automatically captures it:
156+
```python
157+
logger.exception(f"Couldn't get row from record id {record_id}")
158+
```
159+
160+
---
161+
162+
### 9. Deprecated asyncio.wait Usage
163+
164+
**Severity:** Medium
165+
**Location:** `pybot/endpoints/slack/events.py:40`
166+
167+
```python
168+
await asyncio.wait(futures) # futures is a list of coroutines
169+
```
170+
171+
**Problem:** In Python 3.10+, passing coroutines directly to `asyncio.wait()` is deprecated. Should pass Task objects or use `asyncio.gather()`.
172+
173+
**Fix:** Use `asyncio.gather(*futures)` instead.
174+
175+
---
176+
177+
### 10. Unreachable Code in Backend Auth
178+
179+
**Severity:** Medium
180+
**Location:** `pybot/endpoints/slack/utils/event_utils.py:111-114`
181+
182+
```python
183+
if 400 <= response.status:
184+
logger.exception("Failed to authenticate with backend")
185+
return {}
186+
response.raise_for_status() # Never reached when status >= 400
187+
```
188+
189+
**Problem:** `raise_for_status()` is unreachable after the early return. This isn't a crash risk but indicates confused error handling logic.
190+
191+
**Fix:** Remove the redundant `raise_for_status()` call.
192+
193+
---
194+
195+
### 11. Decorator Only Catches SlackAPIError
196+
197+
**Severity:** Medium
198+
**Location:** `pybot/endpoints/slack/utils/general_utils.py:9-39`
199+
200+
```python
201+
@functools.wraps(func)
202+
async def handler(command: Command, app: SirBot, *args, **kwargs):
203+
try:
204+
await func(command, app, *args, **kwargs)
205+
except SlackAPIError:
206+
# Only catches SlackAPIError - other exceptions propagate!
207+
```
208+
209+
**Problem:** Commands can fail with `KeyError`, `IndexError`, `aiohttp.ClientError`, etc. These are not caught and crash the handler.
210+
211+
**Fix:** Add broader exception handling or create specific decorators for different error types.
212+
213+
---
214+
215+
### 12. Invalid Type Annotation
216+
217+
**Severity:** Low
218+
**Location:** `pybot/endpoints/slack/message_templates/mentor_request.py:39`
219+
220+
```python
221+
@property
222+
def skillsets(self) -> [str]: # Invalid syntax
223+
```
224+
225+
**Problem:** `[str]` is not valid type annotation syntax. Should be `list[str]`.
226+
227+
**Fix:** Change to `list[str]`.
228+
229+
---
230+
231+
### 13. Airtable get Method Missing Error Check
232+
233+
**Severity:** Medium
234+
**Location:** `pybot/plugins/airtable/api.py:18-22`
235+
236+
```python
237+
async def get(self, url, **kwargs):
238+
async with self.session.get(url, headers=auth_header, **kwargs) as r:
239+
return await r.json() # No status check - returns error JSON silently
240+
```
241+
242+
**Problem:** Unlike `patch`, the `get` method doesn't call `raise_for_status()`. Error responses are returned as normal data.
243+
244+
**Fix:** Add `r.raise_for_status()` or check `ok` field in response.
245+
246+
---
247+
248+
### 14. Potential Division Issues in Pagination
249+
250+
**Severity:** Low
251+
**Location:** `pybot/plugins/airtable/api.py:35-43`
252+
253+
```python
254+
async def _depaginate_records(self, url, params, offset):
255+
records = []
256+
while offset:
257+
params["offset"] = offset
258+
response = await self.get(url, params=params)
259+
records.extend(response["records"]) # KeyError if error response
260+
```
261+
262+
**Problem:** If Airtable returns an error during pagination, `response["records"]` raises KeyError.
263+
264+
**Fix:** Add error checking within the pagination loop.
265+
266+
---
267+
268+
### 15. Missing YELP_TOKEN Validation
269+
270+
**Severity:** Low
271+
**Location:** `pybot/endpoints/slack/utils/__init__.py:12`
272+
273+
```python
274+
YELP_TOKEN = os.environ.get("YELP_TOKEN", "token")
275+
```
276+
277+
**Problem:** Defaults to literal string "token" which will fail Yelp API auth silently, returning error responses that may crash downstream code.
278+
279+
**Fix:** Either require the token or handle missing token gracefully in the lunch command.
280+
281+
---
282+
283+
## Phased Remediation Plan
284+
285+
### Phase 1: Critical Crash Prevention (Immediate)
286+
287+
**Goal:** Prevent the most likely crashes in production.
288+
289+
| Issue # | Description | File | Estimated Effort |
290+
|---------|-------------|------|------------------|
291+
| 1 | Empty businesses array crash | slash_lunch.py | 15 min |
292+
| 2 | Unhandled Yelp API errors | commands.py | 20 min |
293+
| 4 | Empty service records IndexError | mentor_request.py | 15 min |
294+
| 6 | Empty mentor dict KeyError | airtable/utils.py | 15 min |
295+
296+
**Deliverable:** PR with defensive checks for critical data access patterns.
297+
298+
---
299+
300+
### Phase 2: Error Handling Improvements (Short-term)
301+
302+
**Goal:** Improve error handling in API integrations.
303+
304+
| Issue # | Description | File | Estimated Effort |
305+
|---------|-------------|------|------------------|
306+
| 3 | Missing email KeyError | Multiple files | 30 min |
307+
| 5 | Airtable API response errors | api.py | 30 min |
308+
| 7 | Mentor request flow errors | requests.py | 30 min |
309+
| 11 | Decorator exception coverage | general_utils.py | 20 min |
310+
| 13 | Airtable get error check | api.py | 15 min |
311+
312+
**Deliverable:** PR with comprehensive error handling for external API calls.
313+
314+
---
315+
316+
### Phase 3: Code Quality Fixes (Medium-term)
317+
318+
**Goal:** Fix incorrect patterns and deprecation warnings.
319+
320+
| Issue # | Description | File | Estimated Effort |
321+
|---------|-------------|------|------------------|
322+
| 8 | Incorrect exception logging | Multiple files | 15 min |
323+
| 9 | Deprecated asyncio.wait | events.py | 10 min |
324+
| 10 | Unreachable code | event_utils.py | 5 min |
325+
| 12 | Invalid type annotation | mentor_request.py | 5 min |
326+
| 14 | Pagination error handling | api.py | 20 min |
327+
328+
**Deliverable:** PR with code quality improvements and deprecation fixes.
329+
330+
---
331+
332+
### Phase 4: Configuration & Validation (Long-term)
333+
334+
**Goal:** Improve configuration validation and startup checks.
335+
336+
| Issue # | Description | File | Estimated Effort |
337+
|---------|-------------|------|------------------|
338+
| 15 | YELP_TOKEN validation | __init__.py | 20 min |
339+
| - | Add startup validation for required env vars | __main__.py | 30 min |
340+
| - | Add health check for external service connectivity | endpoints | 45 min |
341+
342+
**Deliverable:** PR with configuration validation and improved observability.
343+
344+
---
345+
346+
## Testing Recommendations
347+
348+
1. **Add unit tests for edge cases:**
349+
- Empty Yelp API responses
350+
- Users without email addresses
351+
- Missing Airtable records
352+
- API error responses
353+
354+
2. **Add integration tests:**
355+
- Mock external API failures
356+
- Test error message delivery to users
357+
358+
3. **Add error tracking:**
359+
- Ensure Sentry captures all unhandled exceptions
360+
- Add custom context for debugging
361+
362+
---
363+
364+
## Appendix: Quick Reference
365+
366+
### Files Requiring Changes
367+
368+
| File | Issues |
369+
|------|--------|
370+
| `pybot/endpoints/slack/utils/slash_lunch.py` | #1 |
371+
| `pybot/endpoints/slack/commands.py` | #2 |
372+
| `pybot/endpoints/slack/utils/event_utils.py` | #3, #10 |
373+
| `pybot/endpoints/slack/actions/mentor_request.py` | #3, #8 |
374+
| `pybot/endpoints/slack/message_templates/mentor_request.py` | #4, #12 |
375+
| `pybot/plugins/airtable/api.py` | #5, #8, #13, #14 |
376+
| `pybot/endpoints/airtable/utils.py` | #6 |
377+
| `pybot/endpoints/airtable/requests.py` | #7 |
378+
| `pybot/endpoints/slack/utils/general_utils.py` | #11 |
379+
| `pybot/endpoints/slack/events.py` | #9 |
380+
| `pybot/endpoints/slack/utils/__init__.py` | #15 |

0 commit comments

Comments
 (0)