Skip to content

Commit 2dd3ce1

Browse files
committed
Fix Freebuff 429 error messaging
1 parent aa6555f commit 2dd3ce1

4 files changed

Lines changed: 92 additions & 0 deletions

File tree

cli/src/hooks/helpers/send-message.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ import {
1616
getFreebuffGateErrorKind,
1717
isOutOfCreditsError,
1818
isFreeModeUnavailableError,
19+
isFreeModeRateLimitedError,
20+
isRateLimitError,
1921
OUT_OF_CREDITS_MESSAGE,
2022
FREE_MODE_UNAVAILABLE_MESSAGE,
23+
FREEBUFF_RATE_LIMIT_MESSAGE,
2124
} from '../../utils/error-handling'
2225
import { formatElapsedTime } from '../../utils/format-elapsed-time'
2326
import { processImagesForMessage } from '../../utils/image-processor'
@@ -417,6 +420,16 @@ export const handleRunCompletion = (params: {
417420
return
418421
}
419422

423+
if (
424+
IS_FREEBUFF &&
425+
isRateLimitError(output) &&
426+
!isFreeModeRateLimitedError(output)
427+
) {
428+
updater.setError(FREEBUFF_RATE_LIMIT_MESSAGE)
429+
finalizeAfterError()
430+
return
431+
}
432+
420433
// Pass the raw error message to setError (displayed in UserErrorBanner without additional wrapper formatting)
421434
updater.setError(output.message ?? DEFAULT_RUN_OUTPUT_ERROR_MESSAGE)
422435

@@ -517,6 +530,15 @@ export const handleRunError = (params: {
517530
return
518531
}
519532

533+
if (
534+
IS_FREEBUFF &&
535+
isRateLimitError(error) &&
536+
!isFreeModeRateLimitedError(error)
537+
) {
538+
updater.setError(FREEBUFF_RATE_LIMIT_MESSAGE)
539+
return
540+
}
541+
520542
// Use setError for all errors so they display in UserErrorBanner consistently
521543
const errorMessage = errorInfo.message || 'An unexpected error occurred'
522544
updater.setError(errorMessage)

cli/src/utils/__tests__/error-handling.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import { describe, test, expect } from 'bun:test'
33
import {
44
isOutOfCreditsError,
55
isFreeModeUnavailableError,
6+
isFreeModeRateLimitedError,
7+
isRateLimitError,
68
getCountryBlockFromFreeModeError,
79
OUT_OF_CREDITS_MESSAGE,
810
FREE_MODE_UNAVAILABLE_MESSAGE,
11+
FREEBUFF_RATE_LIMIT_MESSAGE,
912
createErrorMessage,
1013
} from '../error-handling'
1114

@@ -115,6 +118,42 @@ describe('error-handling', () => {
115118
})
116119
})
117120

121+
describe('isRateLimitError', () => {
122+
test('returns true for error with statusCode 429', () => {
123+
const error = { statusCode: 429, message: 'Too Many Requests' }
124+
expect(isRateLimitError(error)).toBe(true)
125+
})
126+
127+
test('returns true for thrown API error with status 429', () => {
128+
const error = { status: 429, message: 'Too Many Requests' }
129+
expect(isRateLimitError(error)).toBe(true)
130+
})
131+
132+
test('returns false for non-429 status codes', () => {
133+
expect(isRateLimitError({ statusCode: 402 })).toBe(false)
134+
expect(isRateLimitError({ statusCode: 500 })).toBe(false)
135+
})
136+
137+
test('returns false for string statusCode', () => {
138+
expect(isRateLimitError({ statusCode: '429' })).toBe(false)
139+
})
140+
})
141+
142+
describe('isFreeModeRateLimitedError', () => {
143+
test('returns true for typed free mode rate limit errors', () => {
144+
expect(
145+
isFreeModeRateLimitedError({
146+
statusCode: 429,
147+
error: 'free_mode_rate_limited',
148+
}),
149+
).toBe(true)
150+
})
151+
152+
test('returns false for untyped provider rate limit errors', () => {
153+
expect(isFreeModeRateLimitedError({ statusCode: 429 })).toBe(false)
154+
})
155+
})
156+
118157
describe('getCountryBlockFromFreeModeError', () => {
119158
test('extracts country block details from free-mode unavailable errors', () => {
120159
const error = {
@@ -177,6 +216,15 @@ describe('error-handling', () => {
177216
})
178217
})
179218

219+
describe('FREEBUFF_RATE_LIMIT_MESSAGE', () => {
220+
test('encourages retry without mentioning credits or payment', () => {
221+
const message = FREEBUFF_RATE_LIMIT_MESSAGE.toLowerCase()
222+
expect(message).toContain('try again')
223+
expect(message).not.toContain('credit')
224+
expect(message).not.toContain('pay')
225+
})
226+
})
227+
180228
describe('createErrorMessage', () => {
181229
test('creates message from Error object', () => {
182230
const error = new Error('Something went wrong')

cli/src/utils/error-handling.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,22 @@ export const isFreeModeUnavailableError = (error: unknown): boolean => {
6161
return false
6262
}
6363

64+
/**
65+
* Check if an error indicates rate limiting.
66+
* Agent outputs use statusCode; some thrown API errors use status.
67+
*/
68+
export const isRateLimitError = (error: unknown): boolean => {
69+
if (!error || typeof error !== 'object') return false
70+
const statusCode = (error as { statusCode?: unknown }).statusCode
71+
const status = (error as { status?: unknown }).status
72+
return statusCode === 429 || status === 429
73+
}
74+
75+
export const isFreeModeRateLimitedError = (error: unknown): boolean => {
76+
if (!isRateLimitError(error)) return false
77+
return (error as { error?: unknown }).error === 'free_mode_rate_limited'
78+
}
79+
6480
export const getCountryBlockFromFreeModeError = (
6581
error: unknown,
6682
): {
@@ -134,6 +150,9 @@ export const getFreebuffGateErrorKind = (
134150

135151
export const OUT_OF_CREDITS_MESSAGE = `Out of credits. Please add credits at ${defaultAppUrl}/usage`
136152

153+
export const FREEBUFF_RATE_LIMIT_MESSAGE =
154+
'Freebuff is temporarily busy. Please try again in a moment.'
155+
137156
export const FREE_MODE_UNAVAILABLE_MESSAGE = IS_FREEBUFF
138157
? 'Freebuff is not available in your country.'
139158
: 'Free mode is not available in your country. You can use another mode to continue.'

docs/error-schema.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ isOutOfCreditsError(output) → shows OUT_OF_CREDITS_MESSAGE
161161

162162
// Checks statusCode === 403 && error === 'free_mode_unavailable'
163163
isFreeModeUnavailableError(output) → shows FREE_MODE_UNAVAILABLE_MESSAGE
164+
165+
// Freebuff only: checks statusCode === 429 after waiting-room/free-mode quota errors
166+
isRateLimitError(output) → shows FREEBUFF_RATE_LIMIT_MESSAGE
164167
```
165168

166169
For all other errors, the raw `output.message` is displayed in the `UserErrorBanner`.

0 commit comments

Comments
 (0)