1use async_trait::async_trait;
34use serde_json::{json, Value};
35
36use crate::error::{FlowError, Result};
37use crate::node::{ExecContext, Node};
38
39pub struct HttpRequestNode;
43
44#[async_trait]
45impl Node for HttpRequestNode {
46 fn node_type(&self) -> &str {
47 "http-request"
48 }
49
50 async fn execute(&self, ctx: ExecContext) -> Result<Value> {
51 let data = &ctx.data;
52
53 let url = data["url"]
54 .as_str()
55 .ok_or_else(|| FlowError::InvalidDefinition("http-request: missing data.url".into()))?;
56
57 let method = data["method"].as_str().unwrap_or("GET");
58
59 let client = reqwest::Client::new();
60 let mut req = match method.to_ascii_uppercase().as_str() {
61 "GET" => client.get(url),
62 "POST" => client.post(url),
63 "PUT" => client.put(url),
64 "DELETE" => client.delete(url),
65 "PATCH" => client.patch(url),
66 other => {
67 return Err(FlowError::InvalidDefinition(format!(
68 "http-request: unsupported method '{other}'"
69 )))
70 }
71 };
72
73 if let Some(headers) = data["headers"].as_object() {
74 for (name, val) in headers {
75 if let Some(v) = val.as_str() {
76 req = req.header(name.as_str(), v);
77 }
78 }
79 }
80
81 if let Some(body) = data.get("body") {
82 if !body.is_null() {
83 req = req.json(body);
84 }
85 }
86
87 let response = req
88 .send()
89 .await
90 .map_err(|e| FlowError::Internal(format!("http-request: request failed: {e}")))?;
91
92 let status = response.status().as_u16();
93 let ok = response.status().is_success();
94
95 let text = response.text().await.map_err(|e| {
96 FlowError::Internal(format!("http-request: failed to read response: {e}"))
97 })?;
98
99 let body: Value = serde_json::from_str(&text).unwrap_or(Value::String(text));
100
101 Ok(json!({ "status": status, "ok": ok, "body": body }))
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use std::collections::HashMap;
109
110 #[tokio::test]
111 async fn rejects_missing_url() {
112 let err = HttpRequestNode
113 .execute(ExecContext {
114 data: json!({}),
115 inputs: HashMap::new(),
116 variables: HashMap::new(),
117 ..Default::default()
118 })
119 .await
120 .unwrap_err();
121 assert!(matches!(err, FlowError::InvalidDefinition(_)));
122 }
123
124 #[tokio::test]
125 async fn rejects_unsupported_method() {
126 let err = HttpRequestNode
127 .execute(ExecContext {
128 data: json!({ "url": "http://example.com", "method": "HEAD" }),
129 inputs: HashMap::new(),
130 variables: HashMap::new(),
131 ..Default::default()
132 })
133 .await
134 .unwrap_err();
135 assert!(matches!(err, FlowError::InvalidDefinition(_)));
136 }
137}