From 42920c7343c99446fc026926b3fccafe805e6567 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Wed, 29 Apr 2026 17:21:03 +0545 Subject: [PATCH] fix(OUT-3667): retry transient Dropbox 5xx and wrap getDropboxFileMetadata Sentry DROPBOX-INTEGRATION-G surfaces a transient Dropbox 504 from DropboxWebhook.getDropboxFileMetadata because the call wasn't wrapped in retry, and withRetry's retryable set excluded 502/503/504. Broaden the retryable codes and route filesGetMetadata through withRetry. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../webhook/dropbox/lib/webhook.service.ts | 7 ++-- src/lib/withRetry.ts | 36 +++++++------------ 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/features/webhook/dropbox/lib/webhook.service.ts b/src/features/webhook/dropbox/lib/webhook.service.ts index a584995..9ec5b57 100644 --- a/src/features/webhook/dropbox/lib/webhook.service.ts +++ b/src/features/webhook/dropbox/lib/webhook.service.ts @@ -17,6 +17,7 @@ import { generateToken } from '@/lib/copilot/generateToken' import User from '@/lib/copilot/models/User.model' import { DropboxClient } from '@/lib/dropbox/DropboxClient' import logger from '@/lib/logger' +import { withRetry } from '@/lib/withRetry' import { handleChannelFileChanges, processDropboxChanges } from '@/trigger/processFileSync' const DEBOUNCE_WINDOW_MS = 5 * 60 * 1000 // 5 minutes @@ -112,9 +113,11 @@ export class DropboxWebhook { await db.update(dropboxConnections).set({ lastWebhookSyncedAt: new Date() }).where(conditions) } + // Refactor below code. Move the function to DropboxClient file and call it from here. async getDropboxFileMetadata(filePath: string, dbxClient: Dropbox) { - return await dbxClient.filesGetMetadata({ - path: filePath, + return await withRetry((path: string) => dbxClient.filesGetMetadata({ path }), [filePath], { + minTimeout: 3000, + maxTimeout: 12000, }) } diff --git a/src/lib/withRetry.ts b/src/lib/withRetry.ts index 9373f32..2d25353 100644 --- a/src/lib/withRetry.ts +++ b/src/lib/withRetry.ts @@ -7,6 +7,14 @@ import pRetry from 'p-retry' import type { StatusableError } from '@/errors/BaseServerError' import { sleep } from '@/utils/sleep' +const RETRYABLE_STATUS_CODES = new Set([ + httpStatus.TOO_MANY_REQUESTS, + httpStatus.INTERNAL_SERVER_ERROR, + httpStatus.BAD_GATEWAY, + httpStatus.SERVICE_UNAVAILABLE, + httpStatus.GATEWAY_TIMEOUT, +]) + export const withRetry = async ( fn: (...args: Args) => Promise, args: Args, @@ -63,18 +71,8 @@ export const withRetry = async ( onFailedAttempt: (error: { error: unknown; attemptNumber: number; retriesLeft: number }) => { if (error.error instanceof DropboxResponseError) { - const errorStatus = error.error.status - if ( - errorStatus !== httpStatus.TOO_MANY_REQUESTS && - errorStatus !== httpStatus.INTERNAL_SERVER_ERROR - ) - return - } - - if ( - (error.error as StatusableError).status !== httpStatus.TOO_MANY_REQUESTS && - (error.error as StatusableError).status !== httpStatus.INTERNAL_SERVER_ERROR - ) { + if (!RETRYABLE_STATUS_CODES.has(error.error.status)) return + } else if (!RETRYABLE_STATUS_CODES.has((error.error as StatusableError).status)) { return } console.warn( @@ -84,21 +82,11 @@ export const withRetry = async ( }, shouldRetry: (error: { error: unknown; attemptNumber: number; retriesLeft: number }) => { if (error.error instanceof DropboxResponseError) { - const errorStatus = error.error.status - return ( - errorStatus === httpStatus.TOO_MANY_REQUESTS || - errorStatus === httpStatus.INTERNAL_SERVER_ERROR - ) + return RETRYABLE_STATUS_CODES.has(error.error.status) } // Typecasting because Copilot doesn't export an error class - const err = error.error as StatusableError - - // Retry only if statusCode indicates a ratelimit or Internal Server Error - return ( - err.status === httpStatus.TOO_MANY_REQUESTS || - err.status === httpStatus.INTERNAL_SERVER_ERROR - ) + return RETRYABLE_STATUS_CODES.has((error.error as StatusableError).status) }, }, )