featherbit/plugins/native/
error_handler.rs1use async_trait::async_trait;
5use bytes::Bytes;
6use std::collections::HashMap;
7
8use crate::context::Context;
9use crate::plugins::{Plugin, PluginOutput, PluginResult};
10
11pub struct ErrorHandlerPlugin {
17 status_code: u16,
18 body_template: String,
19}
20
21impl ErrorHandlerPlugin {
22 pub fn from_config(config: &HashMap<String, serde_json::Value>) -> Result<Self, String> {
39 let status_code = config
40 .get("status_code")
41 .and_then(|v| v.as_u64())
42 .unwrap_or(500) as u16;
43
44 let body_template = config
45 .get("body_template")
46 .and_then(|v| v.as_str())
47 .unwrap_or(r#"{"error": "internal_error", "message": "An unexpected error occurred"}"#)
48 .to_string();
49
50 Ok(Self {
51 status_code,
52 body_template,
53 })
54 }
55}
56
57#[async_trait]
58impl Plugin for ErrorHandlerPlugin {
59 fn plugin_type(&self) -> &str {
60 "error-handler"
61 }
62
63 async fn execute(
64 &self,
65 mut ctx: Context,
66 _named_inputs: &HashMap<String, serde_json::Value>,
67 ) -> PluginResult {
68 let body = if let Some(last_error) = ctx.errors.last() {
70 self.body_template
71 .replace("{{error.code}}", &last_error.code)
72 .replace("{{error.message}}", &last_error.message)
73 .replace("{{error.node_id}}", &last_error.node_id)
74 } else {
75 self.body_template.clone()
76 };
77
78 ctx.response.status_code = self.status_code;
79 ctx.response.body = Bytes::from(body);
80 ctx.response.headers.insert(
81 "content-type".to_string(),
82 vec!["application/json".to_string()],
83 );
84
85 Ok(PluginOutput {
86 context: ctx,
87 named_outputs: HashMap::new(),
88 })
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
98 use crate::context::{GatewayError, GatewayRequest, GatewayResponse, Protocol};
99
100 fn ctx_with_error(err: Option<GatewayError>) -> Context {
101 Context {
102 request: GatewayRequest {
103 method: "GET".to_string(),
104 path: "/hello".to_string(),
105 host: "h".to_string(),
106 scheme: "http".to_string(),
107 headers: HashMap::new(),
108 query_params: HashMap::new(),
109 body: Bytes::new(),
110 remote_addr: "1.2.3.4:5".to_string(),
111 protocol: Protocol::Http1,
112 },
113 response: GatewayResponse {
114 status_code: 0,
115 headers: HashMap::new(),
116 body: Bytes::new(),
117 },
118 message: HashMap::new(),
119 errors: err.into_iter().collect(),
120 }
121 }
122
123 fn err(code: &str, message: &str, node_id: &str) -> GatewayError {
124 GatewayError {
125 node_id: node_id.to_string(),
126 code: code.to_string(),
127 message: message.to_string(),
128 metadata: HashMap::new(),
129 }
130 }
131
132 fn plugin(config: serde_json::Value) -> ErrorHandlerPlugin {
133 let map: HashMap<String, serde_json::Value> =
134 config.as_object().unwrap().clone().into_iter().collect();
135 ErrorHandlerPlugin::from_config(&map).unwrap()
136 }
137
138 #[tokio::test]
139 async fn test_renders_error_fields_and_status() {
140 let p = plugin(serde_json::json!({
141 "status_code": 502,
142 "body_template": "{\"code\":\"{{error.code}}\",\"msg\":\"{{error.message}}\",\"node\":\"{{error.node_id}}\"}"
143 }));
144 let out = p
145 .execute(
146 ctx_with_error(Some(err("UPSTREAM_ERROR", "connection refused", "backend"))),
147 &HashMap::new(),
148 )
149 .await
150 .unwrap();
151
152 assert_eq!(out.context.response.status_code, 502);
153 let body = String::from_utf8(out.context.response.body.to_vec()).unwrap();
154 assert!(body.contains("UPSTREAM_ERROR"));
155 assert!(body.contains("connection refused"));
156 assert!(body.contains("backend"));
157 assert!(!body.contains("{{"));
159 }
160
161 #[tokio::test]
162 async fn test_uses_the_last_error() {
163 let mut ctx = ctx_with_error(Some(err("FIRST", "first", "a")));
166 ctx.errors.push(err("SECOND", "second", "b"));
167 let out = plugin(serde_json::json!({ "body_template": "{{error.code}}" }))
168 .execute(ctx, &HashMap::new())
169 .await
170 .unwrap();
171 assert_eq!(
172 String::from_utf8(out.context.response.body.to_vec()).unwrap(),
173 "SECOND"
174 );
175 }
176
177 #[tokio::test]
178 async fn test_no_error_leaves_template_literal() {
179 let out = plugin(serde_json::json!({ "body_template": "{{error.code}}" }))
180 .execute(ctx_with_error(None), &HashMap::new())
181 .await
182 .unwrap();
183 assert_eq!(
185 String::from_utf8(out.context.response.body.to_vec()).unwrap(),
186 "{{error.code}}"
187 );
188 }
189}