Skip to content

Commit de451bf

Browse files
committed
fix(connectors): normalize provider throttles
1 parent 5b62952 commit de451bf

6 files changed

Lines changed: 60 additions & 3 deletions

File tree

‎apps/sim/connectors/github/github.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,28 @@ describe('githubConnector.getDocument', () => {
8888
})
8989
})
9090

91+
it('rejects a bodyless blob response instead of misreporting it as oversized', async () => {
92+
const fetchMock = vi
93+
.fn()
94+
.mockResolvedValueOnce(
95+
new Response(
96+
JSON.stringify({
97+
sha: 'blob-sha',
98+
size: 2 * 1024 * 1024,
99+
content: '',
100+
encoding: 'none',
101+
}),
102+
{ status: 200 }
103+
)
104+
)
105+
.mockResolvedValueOnce(new Response(null, { status: 200 }))
106+
vi.stubGlobal('fetch', fetchMock)
107+
108+
await expect(
109+
githubConnector.getDocument('token', { repository: 'owner/repo' }, 'missing-body.md')
110+
).rejects.toThrow('GitHub git blob blob-sha returned no body')
111+
})
112+
91113
it('surfaces a non-rate-limit 403 as a document failure', async () => {
92114
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 403 })))
93115

‎apps/sim/connectors/github/github.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,14 @@ async function fetchBlobContent(
168168
throw new Error(`Failed to fetch git blob ${sha}: ${response.status}`)
169169
}
170170

171+
if (!response.body) {
172+
const contentLength = Number.parseInt(response.headers.get('content-length') ?? '', 10)
173+
if (Number.isFinite(contentLength) && contentLength > MAX_FILE_SIZE) {
174+
throw new ConnectorFileTooLargeError(MAX_FILE_SIZE)
175+
}
176+
throw new Error(`GitHub git blob ${sha} returned no body`)
177+
}
178+
171179
const buffer = await readBodyWithLimit(response, MAX_FILE_SIZE)
172180
if (!buffer) {
173181
throw new ConnectorFileTooLargeError(MAX_FILE_SIZE)

‎apps/sim/connectors/google-drive/google-drive-errors.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@ const PERMISSION_REASONS = new Set([
1212
const POLICY_REASONS = new Set(['domainPolicy', 'download_restricted_for_revision'])
1313
const UNSUPPORTED_EXPORT_REASONS = new Set(['fileNotDownloadable', 'fileNotExportable'])
1414
const QUOTA_REASONS = new Set(['dailyLimitExceeded', 'quotaExceeded'])
15-
const TRANSIENT_REASONS = new Set([
16-
'backendError',
17-
'internalError',
15+
const RATE_LIMIT_REASONS = new Set([
1816
'rateLimitExceeded',
1917
'sharingRateLimitExceeded',
2018
'userRateLimitExceeded',
2119
])
20+
const TRANSIENT_REASONS = new Set(['backendError', 'internalError', ...RATE_LIMIT_REASONS])
2221

2322
export type GoogleDriveErrorKind =
2423
| 'authorization'
@@ -99,6 +98,7 @@ function classifyGoogleDriveError(
9998

10099
export class GoogleDriveApiError extends Error {
101100
retryAfterMs?: number
101+
readonly rateLimited: boolean
102102

103103
constructor(
104104
readonly status: number,
@@ -108,6 +108,7 @@ export class GoogleDriveApiError extends Error {
108108
const reasonSuffix = reasons.length > 0 ? ` (${reasons.join(', ')})` : ''
109109
super(`Google Drive API request failed with HTTP ${status}${reasonSuffix}`)
110110
this.name = 'GoogleDriveApiError'
111+
this.rateLimited = status === 429 || reasons.some((reason) => RATE_LIMIT_REASONS.has(reason))
111112
}
112113
}
113114

‎apps/sim/connectors/google-drive/google-drive.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,18 @@ describe('Google Drive API error parsing', () => {
9494
expect(error.message).not.toContain('upstream unavailable')
9595
})
9696

97+
it('normalizes only structured rate-limit reasons into the shared throttle signal', async () => {
98+
const rateLimit = await readGoogleDriveApiError(
99+
driveErrorResponse('userRateLimitExceeded', 'Provider message')
100+
)
101+
const backendFailure = await readGoogleDriveApiError(
102+
driveErrorResponse('backendError', 'Provider message', 503)
103+
)
104+
105+
expect(rateLimit.rateLimited).toBe(true)
106+
expect(backendFailure.rateLimited).toBe(false)
107+
})
108+
97109
it('omits provider messages from diagnostics', async () => {
98110
const message = `Authorization: Bearer private-token\ncontext ${'x'.repeat(700)}`
99111
const error = await readGoogleDriveApiError(

‎apps/sim/lib/knowledge/documents/utils.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,17 @@ describe('isRateLimitError', () => {
663663
expect(isRateLimitError(new Error('hydration failed', { cause: providerError }))).toBe(true)
664664
})
665665

666+
it('accepts a provider-normalized throttle without HTTP rate-limit headers', () => {
667+
expect(
668+
isRateLimitError(
669+
Object.assign(new Error('Google Drive API request failed with HTTP 403'), {
670+
status: 403,
671+
rateLimited: true,
672+
})
673+
)
674+
).toBe(true)
675+
})
676+
666677
it('does not classify retryable text or transient HTTP failures as provider throttling', () => {
667678
expect(isRateLimitError(new Error('rate limit exceeded'))).toBe(false)
668679
expect(isRateLimitError(Object.assign(new Error('unavailable'), { status: 503 }))).toBe(false)

‎apps/sim/lib/knowledge/documents/utils.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export interface HTTPError extends Error {
2626
status?: number
2727
statusText?: string
2828
retryAfterMs?: number
29+
/** Provider-normalized signal for throttles that do not carry standard HTTP evidence. */
30+
rateLimited?: boolean
2931
/**
3032
* Response headers carried onto the error so the retry loop can re-evaluate
3133
* rate-limit evidence (`isRetryableError` runs again on the thrown error).
@@ -239,6 +241,7 @@ export function isRateLimitError(error: unknown): boolean {
239241

240242
while (isRetryableErrorType(current) && !seen.has(current) && seen.size < 10) {
241243
seen.add(current)
244+
if ((current as HTTPError).rateLimited === true) return true
242245
if (
243246
hasStatus(current) &&
244247
(current.status === 429 ||

0 commit comments

Comments
 (0)