-
Notifications
You must be signed in to change notification settings - Fork 14.2k
fix(core): prevent uncontrolled retry loop during fallback #27008
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
682057a
79ec472
3bed217
85ec0fe
ee4e6b3
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 |
|---|---|---|
|
|
@@ -307,14 +307,31 @@ export async function retryWithBackoff<T>( | |
| ) { | ||
| if (onPersistent429) { | ||
| try { | ||
| const currentModel = getAvailabilityContext?.()?.policy.model; | ||
|
|
||
| const fallbackModel = await onPersistent429( | ||
| authType, | ||
| classifiedError, | ||
| ); | ||
| if (fallbackModel) { | ||
| attempt = 0; // Reset attempts and retry with the new model. | ||
| currentDelay = initialDelayMs; | ||
| continue; | ||
|
|
||
| // Only continue (immediate retry) if fallbackModel is a NEW model | ||
| if ( | ||
| typeof fallbackModel === 'string' && | ||
| fallbackModel !== currentModel | ||
| ) { | ||
| continue; | ||
| } else { | ||
| // If it's the same model (or a boolean retry signal), wait before retrying | ||
| currentDelay = await applyBackoffDelay( | ||
| currentDelay, | ||
| maxDelayMs, | ||
| signal, | ||
| ); | ||
| continue; | ||
| } | ||
| } | ||
|
Comment on lines
+310
to
335
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. The fallback logic in const fallbackModel = await onPersistent429(
authType,
classifiedError,
);
if (fallbackModel) {
const currentContext = getAvailabilityContext?.();
const currentModel = currentContext?.policy.model;
if (
typeof fallbackModel === 'string' &&
fallbackModel !== currentModel
) {
attempt = 0;
currentDelay = initialDelayMs;
continue;
} else {
const jitter = currentDelay * 0.3 * (Math.random() * 2 - 1);
const delayWithJitter = Math.max(0, currentDelay + jitter);
await delay(delayWithJitter, signal);
currentDelay = Math.min(maxDelayMs, currentDelay * 2);
continue;
}
}References
|
||
| } catch (fallbackError) { | ||
| debugLogger.warn('Fallback to Flash model failed:', fallbackError); | ||
|
|
@@ -356,14 +373,31 @@ export async function retryWithBackoff<T>( | |
| ); | ||
| if (onPersistent429) { | ||
| try { | ||
| const currentModel = getAvailabilityContext?.()?.policy.model; | ||
|
|
||
| const fallbackModel = await onPersistent429( | ||
| authType, | ||
| classifiedError, | ||
| ); | ||
| if (fallbackModel) { | ||
| attempt = 0; // Reset attempts and retry with the new model. | ||
| currentDelay = initialDelayMs; | ||
| continue; | ||
|
|
||
| // Only continue (immediate retry) if fallbackModel is a NEW model | ||
| if ( | ||
| typeof fallbackModel === 'string' && | ||
| fallbackModel !== currentModel | ||
| ) { | ||
| continue; | ||
| } else { | ||
| // If it's the same model (or a boolean retry signal), wait before retrying | ||
| currentDelay = await applyBackoffDelay( | ||
| currentDelay, | ||
| maxDelayMs, | ||
| signal, | ||
| ); | ||
| continue; | ||
| } | ||
|
Comment on lines
+387
to
+400
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. This section contains a similar Denial of Service (DoS) vulnerability. The attempt counter and currentDelay are reset before verifying if the fallbackModel is new. This, combined with the backoff delay being skipped if currentModel is undefined, can lead to an infinite loop and broken exponential backoff. This violates the repository rule requiring a mechanism to limit retry attempts to prevent infinite loops. // Only continue (immediate retry) if fallbackModel is a NEW model
if (
typeof fallbackModel === 'string' &&
currentModel !== undefined &&
fallbackModel !== currentModel
) {
continue;
} else {
// If it's the same model (or a boolean retry signal), wait before retrying
currentDelay = await applyBackoffDelay(
currentDelay,
maxDelayMs,
signal,
);
continue;
}References
|
||
| } | ||
|
Comment on lines
+376
to
401
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. This block duplicates the logic flaw where the const fallbackModel = await onPersistent429(
authType,
classifiedError,
);
if (fallbackModel) {
const currentContext = getAvailabilityContext?.();
const currentModel = currentContext?.policy.model;
if (
typeof fallbackModel === 'string' &&
fallbackModel !== currentModel
) {
attempt = 0;
currentDelay = initialDelayMs;
continue;
} else {
const jitter = currentDelay * 0.3 * (Math.random() * 2 - 1);
const delayWithJitter = Math.max(0, currentDelay + jitter);
await delay(delayWithJitter, signal);
currentDelay = Math.min(maxDelayMs, currentDelay * 2);
continue;
}
}References
|
||
| } catch (fallbackError) { | ||
| debugLogger.warn('Model fallback failed:', fallbackError); | ||
|
|
@@ -472,3 +506,14 @@ function logRetryAttempt( | |
| debugLogger.warn(message, error); // Default to warn if error type is unknown | ||
| } | ||
| } | ||
|
|
||
| async function applyBackoffDelay( | ||
| currentDelay: number, | ||
| maxDelayMs: number, | ||
| signal?: AbortSignal, | ||
| ): Promise<number> { | ||
| const jitter = currentDelay * 0.3 * (Math.random() * 2 - 1); | ||
| const delayWithJitter = Math.max(0, currentDelay + jitter); | ||
| await delay(delayWithJitter, signal); | ||
| return Math.min(maxDelayMs, currentDelay * 2); | ||
| } | ||
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.
The retry logic is vulnerable to a Denial of Service (DoS) due to uncontrolled retry loops and non-exponential backoff. This occurs because the attempt counter and currentDelay are reset before checking if the fallbackModel is new. Specifically, if currentModel is undefined, the condition fallbackModel !== currentModel incorrectly evaluates to true, leading to immediate retries without delay. This violates the repository rule requiring a mechanism to limit retry attempts to prevent infinite loops.
References