Skip to main content

txtx_core/std/commands/actions/
http.rs

1use txtx_addon_kit::reqwest::{self, Method};
2use txtx_addon_kit::types::commands::{CommandExecutionFutureResult, PreCommandSpecification};
3use txtx_addon_kit::types::frontend::{Actions, BlockEvent};
4use txtx_addon_kit::types::stores::ValueStore;
5use txtx_addon_kit::types::types::RunbookSupervisionContext;
6use txtx_addon_kit::types::ConstructDid;
7use txtx_addon_kit::types::{
8    commands::{CommandExecutionResult, CommandImplementation, CommandSpecification},
9    diagnostics::Diagnostic,
10    types::{Type, Value},
11};
12use txtx_addon_kit::{define_command, indoc};
13
14lazy_static! {
15    pub static ref SEND_HTTP_REQUEST: PreCommandSpecification = define_command! {
16        SendHttpRequest => {
17            name: "Send an HTTP request",
18            matcher: "send_http_request",
19            documentation: "`std::send_http_request` makes an HTTP request to the given URL and exports the response.",
20            implements_signing_capability: false,
21            implements_background_task_capability: false,
22            inputs: [
23                url: {
24                    documentation: "The URL for the request. Supported schemes are http and https.",
25                    typing: Type::string(),
26                    optional: false,
27                    tainting: true,
28                    internal: false
29                },
30                body: {
31                  documentation: "The request body as a string or json object.",
32                  typing: Type::string(),
33                  optional: true,
34                  tainting: true,
35                  internal: false
36                },
37                method: {
38                  documentation: indoc!{r#"
39                  The HTTP Method for the request. 
40                  Allowed methods are a subset of methods defined in RFC7231: GET, HEAD, and POST. 
41                  POST support is only intended for read-only URLs, such as submitting a search."#},
42                  typing: Type::string(),
43                  optional: true,
44                  tainting: true,
45                  internal: false
46                },
47                timeout_ms: {
48                  documentation: "The request timeout in milliseconds.",
49                  typing: Type::integer(),
50                  optional: true,
51                  tainting: true,
52                  internal: false
53                },
54                headers: {
55                    documentation: "A map of request header field names and values.",
56                    typing: Type::arbitrary_object(),
57                    optional: true,
58                    tainting: true,
59                    internal: false
60                }
61            ],
62            outputs: [
63                response_body: {
64                    documentation: "The response body returned as a string.",
65                    typing: Type::string()
66                },
67                status_code: {
68                    documentation: "The HTTP response status code.",
69                    typing: Type::integer()
70                }
71            ],
72            example: indoc!{r#"
73            action "example" "std::send_http_request" {
74              url = "https://example.com"
75            }
76          
77            output "status" {
78              value = action.example.status_code
79            }
80            // > status: 200
81            "#},
82        }
83    };
84}
85pub struct SendHttpRequest;
86
87impl CommandImplementation for SendHttpRequest {
88    fn check_instantiability(
89        _ctx: &CommandSpecification,
90        _args: Vec<Type>,
91    ) -> Result<Type, Diagnostic> {
92        unimplemented!()
93    }
94
95    fn check_executability(
96        _construct_id: &ConstructDid,
97        _instance_name: &str,
98        _spec: &CommandSpecification,
99        _values: &ValueStore,
100        _supervision_context: &RunbookSupervisionContext,
101        _auth_context: &txtx_addon_kit::types::AuthorizationContext,
102    ) -> Result<Actions, Diagnostic> {
103        Ok(Actions::none())
104    }
105
106    fn run_execution(
107        _construct_id: &ConstructDid,
108        _spec: &CommandSpecification,
109        values: &ValueStore,
110        _progress_tx: &txtx_addon_kit::channel::Sender<BlockEvent>,
111        _auth_ctx: &txtx_addon_kit::types::AuthorizationContext,
112    ) -> CommandExecutionFutureResult {
113        let mut result = CommandExecutionResult::new();
114        let values = values.clone();
115        let url = values.get_expected_string("url")?.to_string();
116        let request_body = values.get_value("body").cloned();
117        let method = {
118            let value = values.get_string("method").unwrap_or("GET");
119            Method::try_from(value).unwrap()
120        };
121        let request_headers = values.get_value("headers").cloned();
122
123        let future = async move {
124            let request_headers = request_headers
125                .as_ref()
126                .map(|value| {
127                    value
128                        .as_object()
129                        .ok_or_else(|| diagnosed_error!("request headers must be an object"))
130                })
131                .transpose()?;
132
133            let client = reqwest::Client::new();
134            let mut req_builder = client.request(method, url);
135
136            if let Some(request_headers) = request_headers {
137                for (k, v) in request_headers.iter() {
138                    req_builder = req_builder.header(
139                        k,
140                        v.as_string().ok_or_else(|| {
141                            diagnosed_error!("request header value must be a string; found type '{}' for header '{}'", v.get_type().to_string(), k)
142                        })?,
143                    );
144                }
145            }
146
147            if let Some(request_body) = request_body {
148                if request_body.as_object().is_some() {
149                    req_builder = req_builder.json(&request_body.to_json(None));
150                } else {
151                    req_builder = req_builder.body(request_body.encode_to_string());
152                }
153            }
154
155            let res = req_builder.send().await.map_err(|e| {
156                Diagnostic::error_from_string(format!("unable to send http request - {e}"))
157            })?;
158
159            let status_code = res.status();
160            let response_body = res.text().await.map_err(|e| {
161                Diagnostic::error_from_string(format!("Failed to parse http request result: {e}"))
162            })?;
163
164            result
165                .outputs
166                .insert(format!("status_code"), Value::integer(status_code.as_u16().into()));
167
168            result.outputs.insert(format!("response_body"), Value::string(response_body));
169
170            Ok::<CommandExecutionResult, Diagnostic>(result)
171        };
172        #[cfg(feature = "wasm")]
173        panic!("async commands are not enabled for wasm");
174        #[cfg(not(feature = "wasm"))]
175        Ok(Box::pin(future))
176    }
177}