Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/app-store/googlecalendar/lib/CalendarAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,21 @@ export class CalendarAuth {

return new calendar_v3.Calendar({
auth: googleAuthClient,
// Override gaxios retry defaults so that transient Google API errors
// on PATCH requests (used to update event description, location, and
// conference data after creation) are retried instead of silently
// dropped. Google returns 403 for rateLimitExceeded in addition to
// 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.

statusCodesToRetry: [
[100, 199],
[403, 403],
[429, 429],
Comment on lines +314 to +317
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.

[500, 599],
],
},
});
}
}
Loading