Skip to main content

ironflow_engine/context/
error_handlers.rs

1//! Error handlers registered on [`WorkflowContext`].
2//!
3//! A handler registered with [`on_error`](WorkflowContext::on_error) fires once
4//! when a later step fails. Execution is best-effort: a handler failure is
5//! logged and never replaces the error that triggered it.
6
7use std::mem::take;
8use std::time::Instant;
9
10use chrono::Utc;
11use serde_json::json;
12use tracing::{info, warn};
13
14use ironflow_store::models::{NewStep, StepStatus, StepUpdate, step_trace_id};
15
16use crate::config::StepConfig;
17use crate::executor::execute_step_config_intercepted;
18use crate::log_sender::StepLogSender;
19
20use super::{OnErrorHandler, WorkflowContext};
21
22impl WorkflowContext {
23    /// Register an error handler that fires when any subsequent step fails.
24    ///
25    /// The handler is consumed after firing (fire-once). Multiple handlers
26    /// can be registered; they fire in registration order.
27    ///
28    /// Error handler execution is best-effort: if a handler fails, the error
29    /// is logged but the original step error is preserved. Error handler steps
30    /// appear in the run timeline with
31    /// [`Step::is_error_handler`](ironflow_store::models::Step::is_error_handler)
32    /// set to `true`.
33    ///
34    /// # Examples
35    ///
36    /// ```no_run
37    /// use ironflow_engine::context::WorkflowContext;
38    /// use ironflow_engine::config::ShellConfig;
39    /// use ironflow_engine::error::EngineError;
40    ///
41    /// # async fn example(ctx: &mut WorkflowContext) -> Result<(), EngineError> {
42    /// ctx.on_error("cleanup", ShellConfig::new("rm -rf /tmp/build"));
43    /// ctx.shell("build", ShellConfig::new("cargo build")).await?;
44    /// # Ok(())
45    /// # }
46    /// ```
47    pub fn on_error(&mut self, name: &str, config: impl Into<StepConfig>) {
48        self.error_handlers.push(OnErrorHandler {
49            name: name.to_string(),
50            config: config.into(),
51        });
52    }
53
54    /// Drain the registered handlers and consume the positions they took in the
55    /// first execution, when a tolerated failure is replayed.
56    pub(super) fn skip_fired_error_handlers(&mut self) {
57        let fired = take(&mut self.error_handlers);
58        self.position += u32::try_from(fired.len()).unwrap_or(u32::MAX);
59    }
60
61    /// Remove all registered error handlers.
62    ///
63    /// # Examples
64    ///
65    /// ```no_run
66    /// use ironflow_engine::context::WorkflowContext;
67    /// use ironflow_engine::config::ShellConfig;
68    /// use ironflow_engine::error::EngineError;
69    ///
70    /// # async fn example(ctx: &mut WorkflowContext) -> Result<(), EngineError> {
71    /// ctx.on_error("cleanup", ShellConfig::new("rm -rf /tmp/build"));
72    /// ctx.shell("build", ShellConfig::new("cargo build")).await?;
73    /// ctx.clear_error_handlers();
74    /// // cleanup will NOT fire if deploy fails
75    /// ctx.shell("deploy", ShellConfig::new("./deploy.sh")).await?;
76    /// # Ok(())
77    /// # }
78    /// ```
79    pub fn clear_error_handlers(&mut self) {
80        self.error_handlers.clear();
81    }
82
83    /// Execute all registered error handlers after a step failure.
84    ///
85    /// Drains the handler list (fire-once). Each handler creates its own
86    /// step record with `is_error_handler = true`. Handler failures are
87    /// logged but never propagated.
88    pub(super) async fn fire_error_handlers(
89        &mut self,
90        failed_step_name: &str,
91        error_msg: &str,
92        duration_ms: u64,
93    ) {
94        let handlers = take(&mut self.error_handlers);
95        if handlers.is_empty() {
96            return;
97        }
98
99        let error_context = json!({
100            "failed_step": failed_step_name,
101            "error": error_msg,
102            "duration_ms": duration_ms,
103        });
104
105        for handler in handlers {
106            let mut config = handler.config.clone();
107            inject_error_context(&mut config, failed_step_name, error_msg, duration_ms);
108
109            let position = self.position;
110            self.position += 1;
111
112            let trace_id = step_trace_id(self.run_id, &handler.name, position);
113            let step = match self
114                .store
115                .create_step(NewStep {
116                    run_id: self.run_id,
117                    trace_id,
118                    name: handler.name.clone(),
119                    kind: config.kind(),
120                    position,
121                    input: Some(error_context.clone()),
122                    is_error_handler: true,
123                })
124                .await
125            {
126                Ok(step) => step,
127                Err(err) => {
128                    warn!(
129                        run_id = %self.run_id,
130                        handler = %handler.name,
131                        error = %err,
132                        "failed to create error handler step"
133                    );
134                    continue;
135                }
136            };
137
138            if let Err(err) = self.start_step(step.id, Utc::now()).await {
139                warn!(
140                    run_id = %self.run_id,
141                    handler = %handler.name,
142                    error = %err,
143                    "failed to start error handler step"
144                );
145                continue;
146            }
147
148            let step_log_sender = self
149                .log_sender
150                .as_ref()
151                .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, handler.name.clone()));
152
153            let start = Instant::now();
154            let result = execute_step_config_intercepted(
155                &config,
156                &self.provider,
157                step_log_sender,
158                self.step_interceptor(),
159            )
160            .await;
161            let handler_duration = start.elapsed().as_millis() as u64;
162            let completed_at = Utc::now();
163
164            match result {
165                Ok(output) => {
166                    if let Err(store_err) = self
167                        .store
168                        .update_step(
169                            step.id,
170                            StepUpdate {
171                                status: Some(StepStatus::Completed),
172                                output: Some(output.output),
173                                duration_ms: Some(handler_duration),
174                                cost_usd: Some(output.cost_usd),
175                                completed_at: Some(completed_at),
176                                ..StepUpdate::default()
177                            },
178                        )
179                        .await
180                    {
181                        warn!(
182                            run_id = %self.run_id,
183                            handler = %handler.name,
184                            error = %store_err,
185                            "failed to persist error handler completion"
186                        );
187                    }
188
189                    info!(
190                        run_id = %self.run_id,
191                        handler = %handler.name,
192                        duration_ms = handler_duration,
193                        "error handler completed"
194                    );
195                }
196                Err(err) => {
197                    if let Err(store_err) = self
198                        .store
199                        .update_step(
200                            step.id,
201                            StepUpdate {
202                                status: Some(StepStatus::Failed),
203                                error: Some(err.to_string()),
204                                duration_ms: Some(handler_duration),
205                                completed_at: Some(completed_at),
206                                ..StepUpdate::default()
207                            },
208                        )
209                        .await
210                    {
211                        warn!(
212                            run_id = %self.run_id,
213                            handler = %handler.name,
214                            error = %store_err,
215                            "failed to persist error handler failure"
216                        );
217                    }
218
219                    warn!(
220                        run_id = %self.run_id,
221                        handler = %handler.name,
222                        error = %err,
223                        "error handler failed (original error preserved)"
224                    );
225                }
226            }
227        }
228    }
229}
230
231/// Inject error context into a step config before executing it as an error handler.
232fn inject_error_context(
233    config: &mut StepConfig,
234    failed_step: &str,
235    error_msg: &str,
236    duration_ms: u64,
237) {
238    match config {
239        StepConfig::Shell(shell) => {
240            shell
241                .env
242                .push(("IRONFLOW_ERROR_STEP".to_string(), failed_step.to_string()));
243            shell
244                .env
245                .push(("IRONFLOW_ERROR_MESSAGE".to_string(), error_msg.to_string()));
246            shell.env.push((
247                "IRONFLOW_ERROR_DURATION_MS".to_string(),
248                duration_ms.to_string(),
249            ));
250        }
251        StepConfig::Agent(agent) => {
252            agent.prompt = format!(
253                "[Error Context]\nStep \"{}\" failed after {}ms:\n{}\n\n{}",
254                failed_step, duration_ms, error_msg, agent.prompt
255            );
256        }
257        StepConfig::Http(http) => {
258            http.headers
259                .push(("X-Ironflow-Error-Step".to_string(), failed_step.to_string()));
260            http.headers.push((
261                "X-Ironflow-Error-Message".to_string(),
262                error_msg.to_string(),
263            ));
264        }
265        StepConfig::Workflow(_)
266        | StepConfig::Approval(_)
267        | StepConfig::Decision(_)
268        | StepConfig::Delay(_) => {}
269    }
270}