Skip to content

Commit 0f503f6

Browse files
committed
fix(trigger): handle Drive rate limits, 410 page token expiry, and clean up comments
1 parent 74d0a47 commit 0f503f6

File tree

1 file changed

+32
-25
lines changed

1 file changed

+32
-25
lines changed

apps/sim/lib/webhooks/polling/google-drive.ts

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,9 @@ export const googleDrivePollingHandler: PollingProviderHandler = {
8989

9090
const config = webhookData.providerConfig as unknown as GoogleDriveWebhookConfig
9191

92-
// First poll: get startPageToken and seed state
92+
// First poll: seed page token and known file set
9393
if (!config.pageToken) {
9494
const startPageToken = await getStartPageToken(accessToken, config, requestId, logger)
95-
9695
await updateWebhookProviderConfig(
9796
webhookId,
9897
{ pageToken: startPageToken, knownFileIds: [] },
@@ -105,7 +104,6 @@ export const googleDrivePollingHandler: PollingProviderHandler = {
105104
return 'success'
106105
}
107106

108-
// Fetch changes since last pageToken
109107
const { changes, newStartPageToken } = await fetchChanges(
110108
accessToken,
111109
config,
@@ -120,7 +118,6 @@ export const googleDrivePollingHandler: PollingProviderHandler = {
120118
return 'success'
121119
}
122120

123-
// Filter changes client-side (folder, MIME type, trashed)
124121
const filteredChanges = filterChanges(changes, config)
125122

126123
if (!filteredChanges.length) {
@@ -145,11 +142,6 @@ export const googleDrivePollingHandler: PollingProviderHandler = {
145142
logger
146143
)
147144

148-
// Update state: new pageToken and rolling knownFileIds.
149-
// Newest IDs are placed first so that when the set exceeds MAX_KNOWN_FILE_IDS,
150-
// the oldest (least recently seen) IDs are evicted. Recent files are more
151-
// likely to be modified again, so keeping them prevents misclassifying a
152-
// repeat modification as a "created" event.
153145
const existingKnownIds = config.knownFileIds || []
154146
const mergedKnownIds = [...new Set([...newKnownFileIds, ...existingKnownIds])].slice(
155147
0,
@@ -180,6 +172,14 @@ export const googleDrivePollingHandler: PollingProviderHandler = {
180172
)
181173
return 'success'
182174
} catch (error) {
175+
if (error instanceof Error && error.name === 'DrivePageTokenInvalidError') {
176+
await updateWebhookProviderConfig(webhookId, { pageToken: undefined }, logger)
177+
await markWebhookSuccess(webhookId, logger)
178+
logger.warn(
179+
`[${requestId}] Drive page token invalid for webhook ${webhookId}, re-seeding on next poll`
180+
)
181+
return 'success'
182+
}
183183
logger.error(`[${requestId}] Error processing Google Drive webhook ${webhookId}:`, error)
184184
await markWebhookFailed(webhookId, logger)
185185
return 'failure'
@@ -204,9 +204,15 @@ async function getStartPageToken(
204204
})
205205

206206
if (!response.ok) {
207+
const status = response.status
207208
const errorData = await response.json().catch(() => ({}))
209+
if (status === 403 || status === 429) {
210+
throw new Error(
211+
`Drive API rate limit (${status}) — skipping to retry next poll cycle: ${JSON.stringify(errorData)}`
212+
)
213+
}
208214
throw new Error(
209-
`Failed to get startPageToken: ${response.status} - ${JSON.stringify(errorData)}`
215+
`Failed to get Drive start page token: ${status} - ${JSON.stringify(errorData)}`
210216
)
211217
}
212218

@@ -227,7 +233,6 @@ async function fetchChanges(
227233
const maxFiles = config.maxFilesPerPoll || MAX_FILES_PER_POLL
228234
let pages = 0
229235

230-
// eslint-disable-next-line no-constant-condition
231236
while (true) {
232237
pages++
233238
const params = new URLSearchParams({
@@ -248,8 +253,19 @@ async function fetchChanges(
248253
})
249254

250255
if (!response.ok) {
256+
const status = response.status
251257
const errorData = await response.json().catch(() => ({}))
252-
throw new Error(`Failed to fetch changes: ${response.status} - ${JSON.stringify(errorData)}`)
258+
if (status === 410) {
259+
const err = new Error('Drive page token is no longer valid')
260+
err.name = 'DrivePageTokenInvalidError'
261+
throw err
262+
}
263+
if (status === 403 || status === 429) {
264+
throw new Error(
265+
`Drive API rate limit (${status}) — skipping to retry next poll cycle: ${JSON.stringify(errorData)}`
266+
)
267+
}
268+
throw new Error(`Failed to fetch Drive changes: ${status} - ${JSON.stringify(errorData)}`)
253269
}
254270

255271
const data = await response.json()
@@ -274,12 +290,9 @@ async function fetchChanges(
274290
currentPageToken = data.nextPageToken as string
275291
}
276292

293+
// When allChanges exceeds maxFiles (multi-page overshoot), resume mid-list via lastNextPageToken.
294+
// Otherwise resume from newStartPageToken (end of change list) or lastNextPageToken (MAX_PAGES hit).
277295
const slicingOccurs = allChanges.length > maxFiles
278-
// Drive API guarantees exactly one of nextPageToken or newStartPageToken per response.
279-
// Slicing case: prefer lastNextPageToken (mid-list resume); fall back to newStartPageToken
280-
// (guaranteed on final page when hasMore was false). Non-slicing case: prefer newStartPageToken
281-
// (guaranteed when loop exhausted all pages); fall back to lastNextPageToken (when loop exited
282-
// early due to MAX_PAGES with hasMore still true).
283296
const resumeToken = slicingOccurs
284297
? (lastNextPageToken ?? newStartPageToken!)
285298
: (newStartPageToken ?? lastNextPageToken!)
@@ -292,26 +305,21 @@ function filterChanges(
292305
config: GoogleDriveWebhookConfig
293306
): DriveChangeEntry[] {
294307
return changes.filter((change) => {
295-
// Always include removals (deletions)
296308
if (change.removed) return true
297309

298310
const file = change.file
299311
if (!file) return false
300312

301-
// Exclude trashed files
302313
if (file.trashed) return false
303314

304-
// Folder filter: check if file is in the specified folder
305315
const folderId = config.folderId || config.manualFolderId
306316
if (folderId) {
307317
if (!file.parents || !file.parents.includes(folderId)) {
308318
return false
309319
}
310320
}
311321

312-
// MIME type filter
313322
if (config.mimeTypeFilter) {
314-
// Support prefix matching (e.g., "image/" matches "image/png", "image/jpeg")
315323
if (config.mimeTypeFilter.endsWith('/')) {
316324
if (!file.mimeType.startsWith(config.mimeTypeFilter)) {
317325
return false
@@ -339,7 +347,6 @@ async function processChanges(
339347
const knownFileIdsSet = new Set(config.knownFileIds || [])
340348

341349
for (const change of changes) {
342-
// Determine event type before idempotency to avoid caching filter decisions
343350
let eventType: 'created' | 'modified' | 'deleted'
344351
if (change.removed) {
345352
eventType = 'deleted'
@@ -349,12 +356,12 @@ async function processChanges(
349356
eventType = 'modified'
350357
}
351358

352-
// Track file as known regardless of filter (for future create/modify distinction)
359+
// Track file as known regardless of filter so future changes are correctly classified
353360
if (!change.removed) {
354361
newKnownFileIds.push(change.fileId)
355362
}
356363

357-
// Client-side event type filter — skip before idempotency so filtered events aren't cached
364+
// Apply event type filter before idempotency so filtered events aren't cached
358365
const filter = config.eventTypeFilter
359366
if (filter) {
360367
const skip = filter === 'created_or_modified' ? eventType === 'deleted' : eventType !== filter

0 commit comments

Comments
 (0)