fix: add retry config for Google Calendar PATCH requests#28879
fix: add retry config for Google Calendar PATCH requests#28879HamzaSwitch wants to merge 1 commit intocalcom:mainfrom
Conversation
The Google Calendar client was created without a retryConfig, so it used gaxios defaults which don't include PATCH in httpMethodsToRetry and don't include 403 in statusCodesToRetry. When Google returns a transient 403 rateLimitExceeded on a PATCH request (used to update event description, location, and conference data after creation), the request was silently dropped — causing calendar/booking desync. Add an explicit retryConfig that includes PATCH in the retryable methods and 403 in the retryable status codes, matching Google's error handling docs which state that rateLimitExceeded can return either 403 or 429. Fixes calcom#28834
📝 WalkthroughWalkthroughThe change modifies the Google Calendar API client initialization in 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/app-store/googlecalendar/lib/CalendarAuth.ts`:
- Line 313: The retry configuration in getClient() within CalendarAuth.ts omits
POST, so write operations like events.insert() invoked by CalendarService.ts
won't be retried on transient 403/429 errors; update the httpMethodsToRetry
array used when constructing the auth/http client to include "POST" (i.e., add
"POST" to httpMethodsToRetry) so write requests are retried similarly to other
methods.
- Around line 314-317: The current statusCodesToRetry array in CalendarAuth.ts
blindly retries all 403 responses (statusCodesToRetry including [403,403]);
change this to scope 403 retries to rate-limit/quota cases only by removing the
static [403,403] entry and implementing a custom retry predicate that inspects
the error payload for Google API error reasons (e.g., "rateLimitExceeded",
"userRateLimitExceeded", "quotaExceeded" or similar) before retrying; update the
retry configuration (the place that uses statusCodesToRetry in CalendarAuth.ts)
to call that predicate so non-rate-limit 403s (auth/scope/access errors) fail
fast.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5b2365a3-1eda-4773-996e-d58bac7e3beb
📒 Files selected for processing (1)
packages/app-store/googlecalendar/lib/CalendarAuth.ts
| // 429 — both should trigger exponential backoff. See #28834. | ||
| retryConfig: { | ||
| retry: 3, | ||
| httpMethodsToRetry: ["GET", "HEAD", "PUT", "OPTIONS", "DELETE", "PATCH"], |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify whether this Google Calendar integration issues POST write calls
# via calendar events APIs on paths that use CalendarAuth.getClient().
rg -n --type=ts -C3 '\.events\.(insert|patch|update)\s*\('Repository: calcom/cal.com
Length of output: 5949
🏁 Script executed:
head -n 330 packages/app-store/googlecalendar/lib/CalendarAuth.ts | tail -n +300Repository: calcom/cal.com
Length of output: 867
🏁 Script executed:
rg -n "CalendarAuth\.getClient\(\)" packages/app-store/googlecalendar/lib/CalendarService.tsRepository: calcom/cal.com
Length of output: 40
🏁 Script executed:
rg -n "getClient\(" packages/app-store/googlecalendar/lib/CalendarAuth.ts | head -20Repository: calcom/cal.com
Length of output: 121
🏁 Script executed:
head -n 50 packages/app-store/googlecalendar/lib/CalendarService.tsRepository: calcom/cal.com
Length of output: 2230
🏁 Script executed:
rg -n "getClient\(\)" packages/app-store/googlecalendar/ --type=tsRepository: calcom/cal.com
Length of output: 270
🏁 Script executed:
sed -n '280,295n' packages/app-store/googlecalendar/lib/CalendarService.tsRepository: calcom/cal.com
Length of output: 40
🏁 Script executed:
rg -n "clientToken|idempotencyToken" packages/app-store/googlecalendar/lib/CalendarService.tsRepository: calcom/cal.com
Length of output: 40
🏁 Script executed:
sed -n '283,290p' packages/app-store/googlecalendar/lib/CalendarService.tsRepository: calcom/cal.com
Length of output: 337
🏁 Script executed:
rg -A10 "\.events\.insert\(\{" packages/app-store/googlecalendar/lib/CalendarService.ts | head -20Repository: calcom/cal.com
Length of output: 527
🏁 Script executed:
web_search
Google Calendar API insert events idempotency token supportRepository: calcom/cal.com
Length of output: 153
🌐 Web query:
Google Calendar API events.insert idempotency support
💡 Result:
No, the Google Calendar API events.insert method does not support built-in idempotency keys or client-specified tokens for preventing duplicate insertions on retry. Unlike some Google APIs (e.g., conferenceData.createRequest.requestId for Meet links), there is no dedicated idempotency parameter. To achieve idempotency: - Generate a client-side unique event ID (UUIDv4 recommended, base32hex chars, 5-1024 length, unique per calendar) and set it in the event body before insert. If the ID already exists in the calendar, the insert fails with a 409 error (duplicate ID). This prevents duplicates from retries or syncs if you map external data to the same ID. - Official docs note: "This enables you to keep entities in your local database in sync with events in Google Calendar. It also prevents duplicate event creation if the operation fails at some point after it is successfully executed in the Calendar backend." - No collision detection guarantee due to distributed system, but UUID minimizes risk. - For retries after success (unknown if created), query events.list first or use your DB to track inserted IDs. Sources confirm no other mechanism; etags are for updates/retrievals, not inserts.
Citations:
- 1: https://developers.google.com/workspace/calendar/api/v3/reference/events/insert
- 2: https://developers.google.com/calendar/api/v3/reference/events/insert
- 3: https://developers.google.com/workspace/calendar/api/v3/reference/events
- 4: https://developers.google.com/calendar/api/guides/create-events
- 5: https://stackoverflow.com/questions/78557900/how-to-prevent-duplicates-from-creating-events-using-google-calendar-api
- 6: https://developers.google.cn/workspace/calendar/api/guides/create-events
- 7: https://stackoverflow.com/questions/9823396/inserting-events-into-google-calendar-is-creating-duplicates-php
Add POST to httpMethodsToRetry to handle transient failures on write operations.
The getClient() method in CalendarAuth.ts is used by CalendarService.ts for events.insert() and other write operations, but POST is excluded from the retry list. Since the insert call does not use idempotency tokens, transient failures (403/429) will not be retried and could silently fail event creation.
Minimal method-list update
- httpMethodsToRetry: ["GET", "HEAD", "PUT", "OPTIONS", "DELETE", "PATCH"],
+ httpMethodsToRetry: ["GET", "HEAD", "PUT", "OPTIONS", "DELETE", "PATCH", "POST"],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| httpMethodsToRetry: ["GET", "HEAD", "PUT", "OPTIONS", "DELETE", "PATCH"], | |
| httpMethodsToRetry: ["GET", "HEAD", "PUT", "OPTIONS", "DELETE", "PATCH", "POST"], |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/app-store/googlecalendar/lib/CalendarAuth.ts` at line 313, The retry
configuration in getClient() within CalendarAuth.ts omits POST, so write
operations like events.insert() invoked by CalendarService.ts won't be retried
on transient 403/429 errors; update the httpMethodsToRetry array used when
constructing the auth/http client to include "POST" (i.e., add "POST" to
httpMethodsToRetry) so write requests are retried similarly to other methods.
There was a problem hiding this comment.
Good catch. I left POST out intentionally because retrying non-idempotent writes risks creating duplicate calendar events. The reporter's bug is specifically about PATCH (the follow-up update after a successful POST/insert). If the team wants POST added too, I can include it in a follow-up — but it'd need a shouldRetry guard that checks for rateLimitExceeded specifically rather than blindly retrying all 403s on POST.
There was a problem hiding this comment.
@HamzaSwitch, that's a solid and well-reasoned design decision. Retrying POST on events.insert() without an idempotency safeguard would risk duplicate calendar events — the Google Calendar API doesn't support a native idempotency token for inserts (unlike e.g. conferenceData.createRequest.requestId), so a blind retry on 403/429 could produce duplicate entries. Scoping the fix to PATCH is the right call for this PR.
If the team does want to pursue POST retry safety in a follow-up, a shouldRetry callback filtering specifically for error.response?.data?.error === "rateLimitExceeded" (while excluding other 403 causes like forbidden or insufficientPermissions) would be the right approach. Happy to help scope that if needed.
✏️ Learnings added
Learnt from: HamzaSwitch
Repo: calcom/cal.com PR: 28879
File: packages/app-store/googlecalendar/lib/CalendarAuth.ts:313-313
Timestamp: 2026-04-14T05:47:00.356Z
Learning: In calcom/cal.com PR `#28879`, `POST` is intentionally excluded from `httpMethodsToRetry` in the `retryConfig` added to `CalendarAuth.getClient()` in `packages/app-store/googlecalendar/lib/CalendarAuth.ts`. The fix targets only `PATCH` (post-create event updates for description/location/conference data). Retrying `POST` (`events.insert`) without an idempotency guard risks creating duplicate calendar events — a safe implementation would require a `shouldRetry` callback that specifically checks `error.response?.data?.error === "rateLimitExceeded"` rather than blindly retrying all 403s. Do not flag the absence of `POST` in `httpMethodsToRetry` as a gap.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: notcodesid
Repo: calcom/cal.com PR: 28867
File: packages/app-store/googlecalendar/lib/CalendarAuth.ts:355-358
Timestamp: 2026-04-13T12:17:26.280Z
Learning: In calcom/cal.com PR `#28867`, the `callback.ts` file in `packages/app-store/googlecalendar/` instantiates `new calendar_v3.Calendar({ auth: oAuth2Client })` without a `retryConfig`. This is intentional: the callback path is only used during OAuth setup to read the primary calendar via `getPrimaryCalendar()` and does not perform any write operations (`insert`/`patch`/`update`). Do not flag the absence of `retryConfig` in `callback.ts` as a gap — it is unrelated to the Google Calendar write-path retry behavior fixed in PR `#28867`.
Learnt from: din-prajapati
Repo: calcom/cal.com PR: 21854
File: packages/app-store/office365calendar/__tests__/unit_tests/SubscriptionManager.test.ts:0-0
Timestamp: 2025-08-05T12:04:29.037Z
Learning: In packages/app-store/office365calendar/lib/CalendarService.ts, the fetcher method in Office365CalendarService class is public, not private. It was specifically changed from private to public in this PR to support proper testing and external access patterns.
Learnt from: yuvrajangadsingh
Repo: calcom/cal.com PR: 26811
File: packages/trpc/server/routers/viewer/slots/util.ts:884-893
Timestamp: 2026-04-08T12:26:02.746Z
Learning: In calcom/cal.com PR `#26811` (feat: check guest availability during host reschedule), the `_getGuestBusyTimesForReschedule` method in `packages/trpc/server/routers/viewer/slots/util.ts` intentionally only checks Cal.com ACCEPTED/PENDING bookings (via `BookingRepository.findBookingsByUserIdsAndDateRange`) rather than full calendar availability (calendar events, schedule rules, OOO/travel). This is a deliberate MVP scoping decision by the author (yuvrajangadsingh); accessing guest calendar credentials for full availability is deferred as a follow-up. Do not flag this as a bug.
Learnt from: Udit-takkar
Repo: calcom/cal.com PR: 22919
File: packages/features/calAIPhone/providers/retellAI/services/PhoneNumberService.ts:212-220
Timestamp: 2025-08-08T10:26:13.362Z
Learning: In calcom/cal.com PR `#22919`, packages/features/calAIPhone/providers/retellAI/services/PhoneNumberService.ts should include the phone number in client-facing HttpError messages (e.g., in updatePhoneNumber/getPhoneNumber catch blocks). Do not suggest redacting the phone number from these errors unless requirements change (per maintainer: Udit-takkar).
Learnt from: Udit-takkar
Repo: calcom/cal.com PR: 22919
File: packages/features/calAIPhone/providers/retellAI/services/AgentService.ts:195-216
Timestamp: 2025-08-08T09:27:23.896Z
Learning: In PR calcom/cal.com#22919, file packages/features/calAIPhone/providers/retellAI/services/AgentService.ts, the updateAgentConfiguration method intentionally does not persist the optional `name` parameter to the repository for now, per maintainer (Udit-takkar). Future reviews should not flag this unless requirements change.
Learnt from: Udit-takkar
Repo: calcom/cal.com PR: 22995
File: packages/trpc/server/routers/viewer/aiVoiceAgent/testCall.handler.ts:41-44
Timestamp: 2025-08-27T12:15:43.830Z
Learning: In calcom/cal.com, the AgentService.getAgent() method in packages/features/calAIPhone/providers/retellAI/services/AgentService.ts does NOT include authorization checks - it only validates the agentId parameter and directly calls the repository without verifying user/team access. This contrasts with other methods like getAgentWithDetails() which properly use findByIdWithUserAccessAndDetails() for authorization. When reviewing updateToolsFromAgentId() calls, always verify both agent ownership and eventType ownership are checked.
| statusCodesToRetry: [ | ||
| [100, 199], | ||
| [403, 403], | ||
| [429, 429], |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In the currently used gaxios/googleapis-common version, does RetryConfig support shouldRetry, and what is the canonical shape of Google Calendar 403 error payloads for rateLimitExceeded/userRateLimitExceeded?
💡 Result:
Yes, in the currently used gaxios version (v7.1.x as of early 2026), RetryConfig supports shouldRetry as an optional function that determines whether to retry a request: (err: GaxiosError) => Promise | boolean. This is used in the retry logic as shown in gaxios/src/retry.ts: const shouldRetryFn = config.shouldRetry || shouldRetryRequest; if (!(await shouldRetryFn(err))) { return {shouldRetry: false, config: err.config}; }. The npm package documentation confirms: shouldRetry?: (err: GaxiosError) => Promise | boolean;. The canonical shape of Google Calendar 403 error payloads is a standard Google APIs error format: For rateLimitExceeded (per-calendar or per-user request rate): { "error": { "errors": [ { "domain": "usageLimits", "reason": "rateLimitExceeded", "message": "Rate Limit Exceeded" } ], "code": 403, "message": "Rate Limit Exceeded" } } Note: Can also return 429 with the same body. For userRateLimitExceeded / quotaExceeded (project/user quotas or calendar usage limits): { "error": { "errors": [ { "domain": "usageLimits", "reason": "userRateLimitExceeded" / "quotaExceeded", "message": "User Rate Limit Exceeded" / "Calendar usage limits exceeded." } ], "code": 403, "message": "User Rate Limit Exceeded" / "Calendar usage limits exceeded." } } In the googleapis client (using gaxios), these appear as GaxiosError with err.response.status === 403 and err.response.data containing the above JSON. Retry logic in gaxios retries 403s only if explicitly configured via shouldRetry or status ranges including 403, but default statusCodesToRetry is [[100,199],[408,408],[429,429],[500,599]] (no 403), so rate limit 403s are not retried by default.
Citations:
- 1: https://github.com/googleapis/gaxios/blob/main/src/retry.ts
- 2: https://www.npmjs.com/package/gaxios
- 3: https://developers.google.com/calendar/api/guides/errors
- 4: https://developers.google.com/calendar/api/guides/quota
- 5: https://github.com/googleapis/gaxios/tags
- 6: https://www.npmjs.com/package/gaxios/v/6.6.0
🏁 Script executed:
# Find where CalendarAuth.getClient() is used and what operations are performed
rg "CalendarAuth\.getClient|authedCalendar" packages/app-store/googlecalendar --type ts --type tsx -A 5 -B 2Repository: calcom/cal.com
Length of output: 85
🏁 Script executed:
# Check CalendarService.ts to see how authedCalendar is used
cat -n packages/app-store/googlecalendar/lib/CalendarService.ts | head -100Repository: calcom/cal.com
Length of output: 4961
🏁 Script executed:
# Search for POST/insert/create operations on the calendar client in googlecalendar
rg "calendar\.events\.(insert|create|patch|update)" packages/app-store/googlecalendar --type ts --type tsx -B 3 -A 3Repository: calcom/cal.com
Length of output: 85
🏁 Script executed:
# Check the current dependency versions for gaxios and googleapis-common
rg '"gaxios"|"googleapis"' packages/app-store/googlecalendar/package.jsonRepository: calcom/cal.com
Length of output: 40
🏁 Script executed:
# Check CalendarAuth.ts to see the exact retryConfig implementation
sed -n '300,330p' packages/app-store/googlecalendar/lib/CalendarAuth.tsRepository: calcom/cal.com
Length of output: 867
🏁 Script executed:
# Correct syntax: search for calendar.events operations (insert, create, patch, update)
rg "calendar\.events\.(insert|create|patch|update)" packages/app-store/googlecalendar -t ts -B 3 -A 3Repository: calcom/cal.com
Length of output: 3909
🏁 Script executed:
# Search for any insert/create calls on the calendar client
rg "\.insert\(|\.create\(" packages/app-store/googlecalendar -t ts -B 2 -A 2Repository: calcom/cal.com
Length of output: 3719
🏁 Script executed:
# Get full CalendarService.ts to see all write operations
wc -l packages/app-store/googlecalendar/lib/CalendarService.tsRepository: calcom/cal.com
Length of output: 118
🏁 Script executed:
# Check what methods use authedCalendar() or getClient()
rg "authedCalendar\(\)" packages/app-store/googlecalendar -t ts -B 2 -A 5Repository: calcom/cal.com
Length of output: 9336
🏁 Script executed:
# Check package.json for gaxios and googleapis versions
cat packages/app-store/googlecalendar/package.json | grep -A 20 "dependencies"Repository: calcom/cal.com
Length of output: 219
🏁 Script executed:
# Also check if there's a root package.json that pins versions
cat package.json | grep -E "gaxios|googleapis-common" | head -20Repository: calcom/cal.com
Length of output: 40
Scope 403 retries to rate-limit reasons only.
[403, 403] retries every forbidden response, including permanent auth/scope/access errors, adding unnecessary retries and delaying hard-failure handling. Filter 403 retries to rate-limit cases only.
Proposed fix
retryConfig: {
retry: 3,
httpMethodsToRetry: ["GET", "HEAD", "PUT", "OPTIONS", "DELETE", "PATCH"],
statusCodesToRetry: [
[100, 199],
[403, 403],
[429, 429],
[500, 599],
],
+ shouldRetry: (err) => {
+ if (err.response?.status !== 403) return true;
+ const googleError = err.response?.data as
+ | { error?: { errors?: Array<{ reason?: string }> } }
+ | undefined;
+ const reasons = googleError?.error?.errors ?? [];
+ return reasons.some(
+ (entry) => entry.reason === "rateLimitExceeded" || entry.reason === "userRateLimitExceeded"
+ );
+ },
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| statusCodesToRetry: [ | |
| [100, 199], | |
| [403, 403], | |
| [429, 429], | |
| retryConfig: { | |
| retry: 3, | |
| httpMethodsToRetry: ["GET", "HEAD", "PUT", "OPTIONS", "DELETE", "PATCH"], | |
| statusCodesToRetry: [ | |
| [100, 199], | |
| [403, 403], | |
| [429, 429], | |
| [500, 599], | |
| ], | |
| shouldRetry: (err) => { | |
| if (err.response?.status !== 403) return true; | |
| const googleError = err.response?.data as | |
| | { error?: { errors?: Array<{ reason?: string }> } } | |
| | undefined; | |
| const reasons = googleError?.error?.errors ?? []; | |
| return reasons.some( | |
| (entry) => entry.reason === "rateLimitExceeded" || entry.reason === "userRateLimitExceeded" | |
| ); | |
| }, | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/app-store/googlecalendar/lib/CalendarAuth.ts` around lines 314 -
317, The current statusCodesToRetry array in CalendarAuth.ts blindly retries all
403 responses (statusCodesToRetry including [403,403]); change this to scope 403
retries to rate-limit/quota cases only by removing the static [403,403] entry
and implementing a custom retry predicate that inspects the error payload for
Google API error reasons (e.g., "rateLimitExceeded", "userRateLimitExceeded",
"quotaExceeded" or similar) before retrying; update the retry configuration (the
place that uses statusCodesToRetry in CalendarAuth.ts) to call that predicate so
non-rate-limit 403s (auth/scope/access errors) fail fast.
Fixes #28834.
The Google Calendar client in
CalendarAuth.getClient()was created without aretryConfig, so it used gaxios defaults which:PATCHinhttpMethodsToRetry(only GET, HEAD, PUT, OPTIONS, DELETE)403instatusCodesToRetry(only 100-199, 429, 500-599)Google returns
403 rateLimitExceededon transient rate limit errors (in addition to429), and the PATCH call that updates event description/location/conference data after creation was never retried. The booking looks successful in Cal.com but the calendar event is missing the updated fields.Added an explicit
retryConfigto thecalendar_v3.Calendarconstructor that includes PATCH in retryable methods and 403 in retryable status codes, matching Google's error handling docs. One file, one config block.