diff --git a/backend/windmill-api/src/flows.rs b/backend/windmill-api/src/flows.rs index d2c93adc9209c..fc4fd9c1db5ae 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -85,6 +85,38 @@ pub fn global_service() -> Router { .route("/hub/get/:id", get(get_hub_flow_by_id)) } +fn validate_flow_value(flow_value: &serde_json::Value) -> Result<()> { + #[cfg(not(feature = "enterprise"))] + if flow_value + .get("ws_error_handler_muted") + .map(|val| val.as_bool().unwrap_or(false)) + .is_some_and(|val| val) + { + return Err(Error::BadRequest( + "Muting the error handler for certain flow is only available in enterprise version" + .to_string(), + )); + } + + if let Some(modules) = flow_value.get("modules").and_then(|m| m.as_array()) { + for module in modules { + if let Some(retry) = module.get("retry") { + if let Some(exponential) = retry.get("exponential") { + let seconds = exponential.get("seconds").and_then(|s| s.as_u64()).ok_or( + Error::BadRequest("Exponential backoff base (seconds) must be a valid positive integer".to_string()), + )?; + if seconds == 0 { + return Err(Error::BadRequest( + "Exponential backoff base (seconds) must be greater than 0. A base of 0 would cause immediate retries.".to_string(), + )); + } + } + } + } + } + Ok(()) +} + #[derive(Serialize, FromRow)] pub struct SearchFlow { path: String, @@ -411,18 +443,8 @@ async fn create_flow( )); } } - #[cfg(not(feature = "enterprise"))] - if nf - .value - .get("ws_error_handler_muted") - .map(|val| val.as_bool().unwrap_or(false)) - .is_some_and(|val| val) - { - return Err(Error::BadRequest( - "Muting the error handler for certain flow is only available in enterprise version" - .to_string(), - )); - } + + validate_flow_value(&nf.value)?; // cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?; let authed = maybe_refresh_folders(&nf.path, &w_id, authed, &db).await; @@ -744,18 +766,7 @@ async fn update_flow( let flow_path = flow_path.to_path(); check_scopes(&authed, || format!("flows:write:{}", flow_path))?; - #[cfg(not(feature = "enterprise"))] - if nf - .value - .get("ws_error_handler_muted") - .map(|val| val.as_bool().unwrap_or(false)) - .is_some_and(|val| val) - { - return Err(Error::BadRequest( - "Muting the error handler for certain flow is only available in enterprise version" - .to_string(), - )); - } + validate_flow_value(&nf.value)?; let authed = maybe_refresh_folders(&flow_path, &w_id, authed, &db).await; let mut tx = user_db.clone().begin(&authed).await?; diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 22e22f2b7a5f4..598474521ee8c 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -80,6 +80,7 @@ import { StepsInputArgs } from './flows/stepsInputArgs.svelte' import { aiChatManager } from './copilot/chat/AIChatManager.svelte' import type { GraphModuleState } from './graph' + import { validateRetryConfig } from '$lib/utils' import { setStepHistoryLoaderContext, StepHistoryLoader, @@ -427,6 +428,15 @@ loadingSave = true try { const flow = cleanInputs(flowStore.val) + + if (flow.value?.modules) { + for (const module of flow.value.modules) { + const error = validateRetryConfig(module.retry) + if (error) { + throw new Error(error) + } + } + } // console.log('flow', computeUnlockedSteps(flow)) // del // loadingSave = false // del // return diff --git a/frontend/src/lib/components/flows/content/FlowRetries.svelte b/frontend/src/lib/components/flows/content/FlowRetries.svelte index 8d14890961317..94c5822334c9b 100644 --- a/frontend/src/lib/components/flows/content/FlowRetries.svelte +++ b/frontend/src/lib/components/flows/content/FlowRetries.svelte @@ -14,6 +14,7 @@ import type { FlowEditorContext } from '../types' import { getStepPropPicker } from '../previousResults' import { NEVER_TESTED_THIS_FAR } from '../models' + import { validateRetryConfig } from '$lib/utils' interface Props { flowModuleRetry: Retry | undefined @@ -59,6 +60,10 @@ : NEVER_TESTED_THIS_FAR ) + let validationError = $derived.by(() => { + return validateRetryConfig(flowModuleRetry) + }) + function setConstantRetries() { flowModuleRetry = { ...flowModuleRetry, @@ -247,7 +252,20 @@ delay = multiplier * base ^ (number of attempt)