Skip to content

Implement Retry with Backoff for Video Generation#27

Closed
mblanc wants to merge 1 commit intomainfrom
feature/video-generation-retry-backoff-795834932259936455
Closed

Implement Retry with Backoff for Video Generation#27
mblanc wants to merge 1 commit intomainfrom
feature/video-generation-retry-backoff-795834932259936455

Conversation

@mblanc
Copy link
Copy Markdown
Owner

@mblanc mblanc commented Mar 3, 2026

This PR implements retry with exponential backoff for video generation, matching the existing behavior for image generation. It also deduplicates the retry and error handling logic by introducing common helpers:

  1. handleApiError in lib/api-utils.ts: Centralizes the logic for mapping Google AI SDK errors (including 429 rate limits) to appropriate HTTP status codes.
  2. withNodeRetry in lib/executors.ts: A higher-order function that wraps node execution with withRetry, handles UI status updates via context.onNodeUpdate, and manages 429-specific messaging.

Both Image and Video executors now use these shared utilities, ensuring consistent behavior and easier maintenance. A new test suite __tests__/retry-executors.test.ts verifies the retry behavior for both node types.


PR created automatically by Jules for task 795834932259936455 started by @mblanc

…te logic

- Extracted `handleApiError` to `lib/api-utils.ts` to centralize SDK error to HTTP status mapping.
- Created `withNodeRetry` helper in `lib/executors.ts` to encapsulate retry logic and UI updates.
- Refactored `executeImageNode` and `executeVideoNode` to use the shared retry helper.
- Added unit tests for executor retry logic.

Co-authored-by: mblanc <662145+mblanc@users.noreply.github.com>
@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the robustness and maintainability of the application's AI generation features. By introducing centralized utilities for API error handling and retry mechanisms with exponential backoff, it ensures a more consistent and resilient user experience, especially during temporary service disruptions or rate limiting. This refactoring also streamlines the codebase, making future development and debugging more efficient.

Highlights

  • Video Generation Retry: Implemented exponential backoff retry logic for video generation, mirroring existing image generation behavior to enhance resilience against transient failures.
  • Centralized API Error Handling: Introduced a new handleApiError utility in lib/api-utils.ts to standardize the mapping of Google AI SDK errors, including 429 rate limits, to appropriate HTTP status codes and consistent error responses.
  • Centralized Node Retry Logic: Created a withNodeRetry higher-order function in lib/executors.ts to encapsulate retry logic for node executions, providing consistent UI status updates and specific messaging for rate-limiting scenarios.
  • Code Deduplication and Consistency: Refactored both image and video generation executors to leverage the new handleApiError and withNodeRetry utilities, ensuring consistent error handling and retry behavior across different generation types.
  • New Test Suite: Added __tests__/retry-executors.test.ts to thoroughly verify the correct retry behavior for both image and video generation nodes, including successful retries and failures after maximum attempts.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • tests/retry-executors.test.ts
    • Added a new test file to verify retry logic for image and video node executors, specifically for 429 (rate limit) errors and maximum retry failures.
  • app/api/generate-image/route.ts
    • Updated the POST handler to use the new handleApiError utility for consistent error response formatting and status code handling.
  • app/api/generate-video/route.ts
    • Updated the POST handler to use the new handleApiError utility for consistent error response formatting and status code handling.
  • lib/api-utils.ts
    • Added a new handleApiError function to centralize error logging, status code determination (especially for 429 errors), and JSON response formatting for API routes.
  • lib/executors.ts
    • Introduced withNodeRetry as a higher-order function to wrap asynchronous operations with retry logic, including exponential backoff, UI updates for retry attempts, and specific error messages for rate limiting.
    • Refactored executeImageNode to utilize withNodeRetry, removing its previous inline retry implementation.
    • Refactored executeVideoNode to utilize withNodeRetry, adding retry capabilities to video generation.
Activity
  • No human activity (comments, reviews, etc.) was detected on this pull request.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements retry with backoff for video generation and refactors shared logic into handleApiError and withNodeRetry helpers, improving maintainability and consistency, and includes tests for the new retry logic. However, the current implementation of handleApiError and the retry logging mechanism are susceptible to information leakage, as raw error messages and full error objects could expose sensitive internal details, API keys, or PII. I recommend sanitizing error messages before sending them to the client and ensuring that logs only contain non-sensitive metadata or sanitized error descriptions. Additionally, I've included a few suggestions to further improve code clarity, reduce duplication, and enhance test performance.

Comment thread lib/executors.ts
Comment on lines +38 to +41
logger.warn(
`[Executor] Attempt ${attempt} failed for node ${nodeId}. Retrying in ${Math.round(delay)}ms...`,
error,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The onRetry callback in withNodeRetry logs the full error object. This can lead to sensitive data exposure in logs if the error object contains secrets or PII. Consider sanitizing error messages before logging to prevent information leakage.

                logger.warn(
                    `[Executor] Attempt ${attempt} failed for node ${nodeId}. Retrying in ${Math.round(delay)}ms...: ${error instanceof Error ? error.message : String(error)}`
                );

Comment thread lib/api-utils.ts
Comment on lines +66 to +72
return NextResponse.json(
{
error: errorMessage,
details: error instanceof Error ? error.message : String(error),
},
{ status },
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The handleApiError function returns the raw error message to the client in both the error and details fields of the JSON response. If the error object contains sensitive information such as internal system paths, database connection strings, or API keys, this information will be exposed to the client. This is a classic information leakage vulnerability (CWE-209).

Suggested change
return NextResponse.json(
{
error: errorMessage,
details: error instanceof Error ? error.message : String(error),
},
{ status },
);
return NextResponse.json(
{
error: errorMessage,
// details: error instanceof Error ? error.message : String(error),
},
{ status },
);

Comment thread lib/api-utils.ts
}

export function handleApiError(error: unknown, fallbackMessage: string) {
logger.error(`[SERVER] ${fallbackMessage}:`, error);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The handleApiError function logs the full error object. If the error object contains sensitive information such as API keys, PII, or internal system details, this information will be stored in the server logs. This can lead to sensitive data exposure in logs (CWE-532).

Suggested change
logger.error(`[SERVER] ${fallbackMessage}:`, error);
logger.error(`[SERVER] ${fallbackMessage}: ${error instanceof Error ? error.message : String(error)}`);

Comment on lines +93 to +125
it("executeVideoNode should fail after maximum retries", async () => {
const mockNode = {
id: "video-1",
data: {
type: "video",
prompt: "a jumping cat",
} as VideoData,
} as Node<VideoData>;

const onNodeUpdate = vi.fn();
const mockFetch = vi.fn().mockResolvedValue({
ok: false,
status: 429,
text: async () => "Rate limit exceeded",
});

// withRetry uses exponential backoff. In tests we might want to mock timers
// but for now let's just see if it fails as expected.
// Note: Default maxRetries is 3 in withNodeRetry.

await expect(
executeVideoNode(
mockNode,
{ prompt: "a jumping cat" },
{
fetch: mockFetch as unknown as typeof fetch,
onNodeUpdate,
},
)
).rejects.toThrow(/High traffic detected/);

expect(mockFetch).toHaveBeenCalledTimes(4); // initial + 3 retries
}, 20000); // Increase timeout for backoff
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This test uses a 20-second timeout to wait for the exponential backoff, which can slow down the test suite. You can use vi.useFakeTimers() to mock the timers and make the test run instantly without relying on real time. This will make your tests faster and more deterministic.

    it("executeVideoNode should fail after maximum retries", async () => {
        vi.useFakeTimers();

        const mockNode = {
            id: "video-1",
            data: {
                type: "video",
                prompt: "a jumping cat",
            } as VideoData,
        } as Node<VideoData>;

        const onNodeUpdate = vi.fn();
        const mockFetch = vi.fn().mockResolvedValue({
            ok: false,
            status: 429,
            text: async () => "Rate limit exceeded",
        });

        const promise = executeVideoNode(
            mockNode,
            { prompt: "a jumping cat" },
            {
                fetch: mockFetch as unknown as typeof fetch,
                onNodeUpdate,
            },
        );

        // Advance timers to allow all retries to execute
        await vi.runAllTimersAsync();

        await expect(promise).rejects.toThrow(/High traffic detected/);

        expect(mockFetch).toHaveBeenCalledTimes(4); // initial + 3 retries

        vi.useRealTimers();
    });

Comment thread lib/api-utils.ts
Comment on lines +47 to +64
if (error instanceof Error) {
errorMessage = error.message;
if (
errorMessage.includes("429") ||
errorMessage.includes("Quota") ||
("status" in error && (error as { status: number }).status === 429)
) {
status = 429;
}
} else if (
typeof error === "object" &&
error !== null &&
"status" in error
) {
if ((error as { status: number }).status === 429) {
status = 429;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for detecting a 429 status code is a bit complex and contains some repetition across the if and else if blocks. You can simplify this by consolidating the checks, which will make the function more readable and easier to maintain.

    if (error instanceof Error) {
        errorMessage = error.message;
    }

    const errorIsObject = typeof error === "object" && error !== null;
    const hasStatus429 =
        errorIsObject &&
        "status" in error &&
        (error as { status: unknown }).status === 429;
    const messageHas429 =
        error instanceof Error &&
        (error.message.includes("429") || error.message.includes("Quota"));

    if (hasStatus429 || messageHas429) {
        status = 429;
    }

@mblanc mblanc closed this Apr 3, 2026
@mblanc mblanc deleted the feature/video-generation-retry-backoff-795834932259936455 branch April 3, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant