-
Notifications
You must be signed in to change notification settings - Fork 12.7k
fix: add retry config for Google Calendar PATCH requests #28879
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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"], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCodesToRetry: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| [100, 199], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| [403, 403], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| [429, 429], | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+314
to
+317
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 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 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.
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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| [500, 599], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
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:
Repository: calcom/cal.com
Length of output: 121
🏁 Script executed:
Repository: 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:
Repository: calcom/cal.com
Length of output: 527
🏁 Script executed:
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
POSTtohttpMethodsToRetryto handle transient failures on write operations.The
getClient()method in CalendarAuth.ts is used by CalendarService.ts forevents.insert()and other write operations, butPOSTis 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
📝 Committable suggestion
🤖 Prompt for AI Agents
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. RetryingPOSTonevents.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 toPATCHis the right call for this PR.If the team does want to pursue POST retry safety in a follow-up, a
shouldRetrycallback filtering specifically forerror.response?.data?.error === "rateLimitExceeded"(while excluding other 403 causes likeforbiddenorinsufficientPermissions) would be the right approach. Happy to help scope that if needed.✏️ Learnings added
🧠 Learnings used