txtx_core/std/commands/actions/
http.rs1use 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}