use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::pipeline::model::{
DslFlag, DslFlagKind, LayoutItem, NodeFieldDef, NodeFieldType, SelectOptionDef,
};
use crate::pipeline::{
NodeDefinition, PipelineError,
nodes::{NodeExecutionInput, NodeExecutionOutput, NodeHandler},
};
pub const NODE_KIND: &str = "n.trigger.weberror";
const INPUT_PIN_IN: &str = "in";
const OUTPUT_PIN_OUT: &str = "out";
pub fn definition() -> NodeDefinition {
NodeDefinition {
kind: NODE_KIND.to_string(),
title: "Web Error Trigger".to_string(),
description: "Triggers a pipeline when an HTTP error occurs. \
Use --code to scope to a specific error code (404, 401), \
a range (4xx, 5xx), or leave empty for a catch-all. \
The pipeline receives error_code, error_message, original_path, and method. \
Pair with n.web.response to serve custom HTML error pages."
.to_string(),
input_schema: json!({
"type": "object",
"properties": {
"error_code": { "type": "integer", "description": "HTTP error code (e.g. 404)." },
"error_message": { "type": "string", "description": "Standard HTTP reason phrase." },
"original_path": { "type": "string", "description": "Path that triggered the error." },
"method": { "type": "string", "description": "HTTP method of the original request." }
}
}),
output_schema: json!({
"type": "object",
"properties": {
"error_code": { "type": "integer" },
"error_message": { "type": "string" },
"original_path": { "type": "string" },
"method": { "type": "string" }
}
}),
input_pins: vec![INPUT_PIN_IN.to_string()],
output_pins: vec![OUTPUT_PIN_OUT.to_string()],
script_available: false,
script_bridge: None,
config_schema: Default::default(),
dsl_flags: vec![DslFlag {
flag: "--code".to_string(),
config_key: "code".to_string(),
description:
"Error code pattern: exact code (404), range (4xx/5xx), or empty for catch-all."
.to_string(),
kind: DslFlagKind::Scalar,
required: false,
}],
fields: vec![NodeFieldDef {
name: "code".to_string(),
label: "Error Code Pattern".to_string(),
field_type: NodeFieldType::Select,
options: vec![
SelectOptionDef {
value: "404".to_string(),
label: "404 — Not Found".to_string(),
},
SelectOptionDef {
value: "4xx".to_string(),
label: "4xx — Any Client Error".to_string(),
},
SelectOptionDef {
value: "5xx".to_string(),
label: "5xx — Any Server Error".to_string(),
},
SelectOptionDef {
value: "*".to_string(),
label: "* — All errors (catch-all)".to_string(),
},
],
help: Some("Which HTTP error code(s) this pipeline handles.".to_string()),
..Default::default()
}],
layout: vec![LayoutItem::Field("code".to_string())],
ai_tool: Default::default(),
..Default::default()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
#[serde(default)]
pub code: String,
}
pub fn match_specificity(code_pattern: &str, error_code: u16) -> Option<u8> {
if code_pattern.is_empty() || code_pattern == "*" {
return Some(0); }
if code_pattern.eq_ignore_ascii_case("4xx") && (400..500).contains(&error_code) {
return Some(1);
}
if code_pattern.eq_ignore_ascii_case("5xx") && (500..600).contains(&error_code) {
return Some(1);
}
if let Ok(exact) = code_pattern.parse::<u16>() {
if exact == error_code {
return Some(2); }
}
None }
pub struct Node {
#[allow(dead_code)]
config: Config,
}
impl Node {
pub fn new(config: Config) -> Self {
Self { config }
}
}
#[async_trait]
impl NodeHandler for Node {
fn kind(&self) -> &'static str {
NODE_KIND
}
fn input_pins(&self) -> &'static [&'static str] {
&[INPUT_PIN_IN]
}
fn output_pins(&self) -> &'static [&'static str] {
&[OUTPUT_PIN_OUT]
}
async fn execute_async(
&self,
input: NodeExecutionInput,
) -> Result<NodeExecutionOutput, PipelineError> {
Ok(NodeExecutionOutput {
output_pins: vec![OUTPUT_PIN_OUT.to_string()],
payload: input.payload,
trace: vec!["n.trigger.weberror: passthrough".to_string()],
})
}
}