Skip to content

fix: improve follow-up prompt timeout and abort handling#6430

Open
Abhiiishek44 wants to merge 7 commits into
FlowiseAI:mainfrom
Abhiiishek44:fix/followup-timeout-retry
Open

fix: improve follow-up prompt timeout and abort handling#6430
Abhiiishek44 wants to merge 7 commits into
FlowiseAI:mainfrom
Abhiiishek44:fix/followup-timeout-retry

Conversation

@Abhiiishek44

Copy link
Copy Markdown

Summary

This PR improves follow-up prompt reliability for Ollama-based requests by making the request flow abort-aware and ensuring cleanup across retry attempts.

Changes included:

  • wrap the Ollama follow-up prompt request in an abort-aware race so configured timeouts and retries are respected consistently
  • ensure AbortSignal listeners are always removed after execution to avoid listener accumulation across retries
  • add a small runtime validation guard for the parsed follow-up prompt response before returning the result

Notes

  • changes are intentionally scoped only to packages/components/src/followUpPrompts.ts
  • no behavior changes for unrelated providers
  • verified under the repository's Node 20 environment
  • local repository-wide TypeScript issues observed during checks appear unrelated to this change

Related: #6421

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

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.

Code Review

This pull request introduces a robust retry mechanism with exponential backoff and timeout handling for generating follow-up prompts across various LLM providers. It adds a new executeWithRetry utility and updates provider-specific logic to utilize this wrapper. Feedback highlights unreachable code at the end of the retry function and redundant timeout logic within the Ollama provider implementation, suggesting a cleaner approach using the existing wrapper's capabilities.

}
}

throw new Error('Follow-up prompt retry attempts exhausted')

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 line is unreachable. The retry loop already throws the last encountered error when attempt >= maxRetries is reached (line 92).

Comment on lines +281 to +332
// Ollama client does not accept AbortSignal directly in this SDK call.
// Wrap the chat promise in a race so the caller-provided signal can
// abort the request and trigger our retry/timeout behavior.
const chatPromise = ollamaClient.chat({
model: providerConfig.modelName,
messages: [
{
role: 'user',
content: followUpPromptsPrompt
}
],
format: {
type: 'object',
properties: {
questions: {
type: 'array',
items: {
type: 'string'
},
minItems: 3,
maxItems: 3,
description: 'Three follow-up questions based on the conversation history'
}
},
minItems: 3,
maxItems: 3,
description: 'Three follow-up questions based on the conversation history'
required: ['questions'],
additionalProperties: false
},
options: {
temperature: parseFloat(`${providerConfig.temperature}`)
}
},
required: ['questions'],
additionalProperties: false
})

let abortHandler: (() => void) | undefined
const abortPromise = new Promise<never>((_, reject) => {
abortHandler = () => {
const timeoutError = new Error('Follow-up prompt request timed out')
timeoutError.name = 'TimeoutError'
reject(timeoutError)
}
signal.addEventListener('abort', abortHandler, { once: true })
})

try {
const response = await Promise.race([chatPromise, abortPromise])
const result = FollowUpPromptType.parse(JSON.parse((response as any).message.content))
if (!result.questions) {
throw new Error('Follow-up prompt response missing questions')
}
return { questions: result.questions }
} finally {
if (abortHandler) signal.removeEventListener('abort', abortHandler)
}

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 manual Promise.race and AbortSignal listener inside the Ollama provider are redundant because executeWithRetry already wraps the execution in a race against a timeout-driven abort signal. Additionally, the check for result.questions is unnecessary as FollowUpPromptType.parse already validates the presence and type of the field, ensuring fail-fast behavior for external data.

                        // Ollama client does not accept AbortSignal directly in this SDK call.
                        // However, executeWithRetry already handles the timeout and abort logic
                        // via Promise.race at the outer level.
                        const response = await ollamaClient.chat({
                            model: providerConfig.modelName,
                            messages: [
                                {
                                    role: 'user',
                                    content: followUpPromptsPrompt
                                }
                            ],
                            format: {
                                type: 'object',
                                properties: {
                                    questions: {
                                        type: 'array',
                                        items: {
                                            type: 'string'
                                        },
                                        minItems: 3,
                                        maxItems: 3,
                                        description: 'Three follow-up questions based on the conversation history'
                                    }
                                },
                                required: ['questions'],
                                additionalProperties: false
                            },
                            options: {
                                temperature: parseFloat(`${providerConfig.temperature}`)
                            }
                        })

                        const result = FollowUpPromptType.parse(JSON.parse((response as any).message.content))
                        return { questions: result.questions }
References
  1. Use a default (fallback) implementation unless the specific implementation has meaningfully different behavior or provides better error messages.
  2. When handling potentially invalid data from external sources, prefer throwing an error for invalid input types rather than silently returning a default or empty value.

@Abhiiishek44

Copy link
Copy Markdown
Author

Hi @jimjimovich, just checking in on this PR.
Could you please review it when you get time? Happy to make any changes if needed. Thanks!


const isRetryableError = (error: any): boolean => {
const status = getErrorStatus(error)
if (status && [429, 500, 502, 503].includes(status)) return true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is a great improvement, but I think the retry classifier should also include timeout HTTP statuses here, especially 504.

Right now 429, 500, 502, and 503 are retryable, and timeout-like messages/codes are retryable, but a provider/proxy error like as Axios' common Request failed with status code 504 can have status = 504 without a timeout-shaped code or message. In that case this would not retry even though 504 Gateway Timeout is one of the potential failures this PR is trying to handle.

Could you, please, include 504 in this list, and maybe 408 as well, with a focused test for at least the 504 path?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for the review. I’ve added 504 and 408 to the retryable HTTP status list and included a focused test for the Axios-style 504 status path.

@Abhiiishek44

Copy link
Copy Markdown
Author

Hi @sviat-barbutsa, just following up on this PR.

I’ve addressed your review feedback by adding 504 and 408 to the retryable HTTP status list, along with a focused test for the Axios-style 504 path.

Could you please re-review it when you get time?

Please let me know if any further changes are needed. Thanks!

@AbhilashG12

Copy link
Copy Markdown

@Abhiiishek44 Hey! I was looking into issue #6421 and tested your branch on my machine. The retry logic for 504/408 works perfectly!

I noticed the PR might be stuck on that automated bot feedback, so I tried applying it on my end to see what happens:

Ignore the bot about executeWithRetry: The bot tells you to delete the throw new Error at the bottom, but if you do, TypeScript gets mad and won't compile. Your original code was right.

Fix the Ollama block: The bot is right about the Ollama part. You can safely delete all that Promise.race and abortPromise code and just await ollamaClient.chat(...). Since your executeWithRetry function is already handling the timeout, you don't need to do it twice.

I tested that Ollama cleanup and the build passes perfectly. Let me know if you want me to paste the exact code snippet here for you to copy/paste. Awesome work getting this built!

@Abhiiishek44

Copy link
Copy Markdown
Author

@AbhilashG12 Thanks for checking this and testing the build locally.

Yes, please paste the snippet here. I’ll review it with my current Ollama block and update the PR accordingly.
Also, thanks for clarifying the executeWithRetry part.

@AbhilashG12

Copy link
Copy Markdown

@Abhiiishek44 No problem at all! Here is the cleaned-up block for the OLLAMA case.

You can replace your entire current const chatPromise = ... block (including the try/finally logic and the Promise.race) with this:

// Ollama client does not accept AbortSignal directly in this SDK call.
// However, executeWithRetry already handles the timeout and abort logic
// via Promise.race at the outer level, making this wrapper redundant.
const response = await ollamaClient.chat({
model: providerConfig.modelName,
messages: [
{
role: 'user',
content: followUpPromptsPrompt
}
],
format: {
type: 'object',
properties: {
questions: {
type: 'array',
items: {
type: 'string'
},
minItems: 3,
maxItems: 3,
description: 'Three follow-up questions based on the conversation history'
}
},
required: ['questions'],
additionalProperties: false
},
options: {
temperature: parseFloat(${providerConfig.temperature})
}
})

const result = FollowUpPromptType.parse(JSON.parse((response as any).message.content))
return { questions: result.questions }

This effectively removes the redundant nested timeout logic while keeping the actual API call logic exactly as you had it. Once you swap that in, your build should pass the linter and the TS compiler perfectly.

Good luck with the PR!

@Abhiiishek44

Copy link
Copy Markdown
Author

Hi @sviat-barbutsa,

In the earlier update, I added 408 and 504 to the retryable status list and included a focused test for the Axios-style 504 case.
In this latest update, I cleaned up the Ollama block based on the automated feedback. I removed the redundant inner timeout handling and kept the outer executeWithRetry flow unchanged, since it already handles timeout and retry behavior.

I also kept the fallback throw new Error(...) in executeWithRetry, as removing it causes TypeScript compilation issues.

Could you please re-review it when you get time?

Thanks!

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.

3 participants