Skip to content

fix: add retry config for Google Calendar PATCH requests#28879

Open
HamzaSwitch wants to merge 1 commit intocalcom:mainfrom
HamzaSwitch:fix/issue-28834-google-calendar-retry-config
Open

fix: add retry config for Google Calendar PATCH requests#28879
HamzaSwitch wants to merge 1 commit intocalcom:mainfrom
HamzaSwitch:fix/issue-28834-google-calendar-retry-config

Conversation

@HamzaSwitch
Copy link
Copy Markdown

Fixes #28834.

The Google Calendar client in CalendarAuth.getClient() was created without a retryConfig, so it used gaxios defaults which:

  1. Don't include PATCH in httpMethodsToRetry (only GET, HEAD, PUT, OPTIONS, DELETE)
  2. Don't include 403 in statusCodesToRetry (only 100-199, 429, 500-599)

Google returns 403 rateLimitExceeded on transient rate limit errors (in addition to 429), 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 retryConfig to the calendar_v3.Calendar constructor that includes PATCH in retryable methods and 403 in retryable status codes, matching Google's error handling docs. One file, one config block.

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
@HamzaSwitch HamzaSwitch requested a review from a team as a code owner April 14, 2026 05:33
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 14, 2026

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions bot added the 🐛 bug Something isn't working label Apr 14, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

📝 Walkthrough

Walkthrough

The change modifies the Google Calendar API client initialization in CalendarAuth.getClient() to include retry configuration for gaxios. The new configuration enables up to 3 automatic retries for idempotent methods and PATCH requests. The retried HTTP status codes now include 403 (rateLimitExceeded), 429 (too many requests), 5xx server errors, and 1xx informational responses. Previously, the client was configured only with authentication, without retry handling for transient errors during PATCH operations used in post-creation updates.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding retry config for Google Calendar PATCH requests.
Description check ✅ Passed The description is directly related to the changeset, explaining the problem, solution, and linking to the related issue #28834.
Linked Issues check ✅ Passed The PR directly addresses all coding requirements from #28834: adds PATCH to retryable HTTP methods, adds 403 to retryable status codes, and includes exponential backoff configuration to the Calendar client.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the identified issue: only modifying CalendarAuth.getClient() to add the necessary retryConfig, with no unrelated alterations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between a657723 and fcdf209.

📒 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"],
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 +300

Repository: calcom/cal.com

Length of output: 867


🏁 Script executed:

rg -n "CalendarAuth\.getClient\(\)" packages/app-store/googlecalendar/lib/CalendarService.ts

Repository: calcom/cal.com

Length of output: 40


🏁 Script executed:

rg -n "getClient\(" packages/app-store/googlecalendar/lib/CalendarAuth.ts | head -20

Repository: calcom/cal.com

Length of output: 121


🏁 Script executed:

head -n 50 packages/app-store/googlecalendar/lib/CalendarService.ts

Repository: calcom/cal.com

Length of output: 2230


🏁 Script executed:

rg -n "getClient\(\)" packages/app-store/googlecalendar/ --type=ts

Repository: calcom/cal.com

Length of output: 270


🏁 Script executed:

sed -n '280,295n' packages/app-store/googlecalendar/lib/CalendarService.ts

Repository: calcom/cal.com

Length of output: 40


🏁 Script executed:

rg -n "clientToken|idempotencyToken" packages/app-store/googlecalendar/lib/CalendarService.ts

Repository: calcom/cal.com

Length of output: 40


🏁 Script executed:

sed -n '283,290p' packages/app-store/googlecalendar/lib/CalendarService.ts

Repository: calcom/cal.com

Length of output: 337


🏁 Script executed:

rg -A10 "\.events\.insert\(\{" packages/app-store/googlecalendar/lib/CalendarService.ts | head -20

Repository: calcom/cal.com

Length of output: 527


🏁 Script executed:

web_search
Google Calendar API insert events idempotency token support

Repository: 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:


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.

Suggested change
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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Comment on lines +314 to +317
statusCodesToRetry: [
[100, 199],
[403, 403],
[429, 429],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


🏁 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 2

Repository: 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 -100

Repository: 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 3

Repository: 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.json

Repository: 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.ts

Repository: 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 3

Repository: 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 2

Repository: 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.ts

Repository: 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 5

Repository: 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 -20

Repository: 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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bug Something isn't working size/S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Google Calendar PATCH requests excluded from retry logic, causing silent booking/calendar desync

2 participants