Skip to main content

a3s_flow/nodes/
http.rs

1//! Built-in `"http-request"` node — executes an HTTP request and returns the
2//! response status, body, and headers.
3//!
4//! Mirrors Dify's HTTP Request node.
5//!
6//! # Config schema
7//!
8//! ```json
9//! {
10//!   "url":     "https://api.example.com/items",
11//!   "method":  "POST",
12//!   "headers": { "Authorization": "Bearer {{token}}" },
13//!   "body":    { "key": "value" }
14//! }
15//! ```
16//!
17//! | Field | Type | Required | Description |
18//! |-------|------|:--------:|-------------|
19//! | `url` | string | ✓ | Request URL |
20//! | `method` | string | | HTTP method — `GET` (default), `POST`, `PUT`, `DELETE`, `PATCH` |
21//! | `headers` | object | | String-valued request headers |
22//! | `body` | any JSON | | Request body (sent as `application/json`) |
23//!
24//! # Output schema
25//!
26//! ```json
27//! { "status": 200, "ok": true, "body": { ... } }
28//! ```
29//!
30//! `body` is parsed as JSON when the Content-Type is JSON; otherwise stored
31//! as a plain string.
32
33use async_trait::async_trait;
34use serde_json::{json, Value};
35
36use crate::error::{FlowError, Result};
37use crate::node::{ExecContext, Node};
38
39/// HTTP request node (Dify-compatible).
40///
41/// Supports `GET`, `POST`, `PUT`, `DELETE`, and `PATCH`.
42pub 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}