1use runtara_agent_macro::{CapabilityInput, CapabilityOutput, capability};
15use runtara_dsl::agent_meta::EnumVariants;
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use std::collections::HashMap;
19use std::io::Read;
20use strum::VariantNames;
21
22#[derive(Debug, Clone, Serialize, Deserialize, VariantNames)]
28#[serde(rename_all = "UPPERCASE")]
29#[strum(serialize_all = "UPPERCASE")]
30pub enum HttpMethod {
31 Get,
33 Post,
35 Put,
37 Delete,
39 Patch,
41 Head,
43 Options,
45}
46
47impl EnumVariants for HttpMethod {
48 fn variant_names() -> &'static [&'static str] {
49 Self::VARIANTS
50 }
51}
52
53impl Default for HttpMethod {
54 fn default() -> Self {
55 Self::Get
56 }
57}
58
59impl HttpMethod {
60 pub fn as_str(&self) -> &str {
61 match self {
62 Self::Get => "GET",
63 Self::Post => "POST",
64 Self::Put => "PUT",
65 Self::Delete => "DELETE",
66 Self::Patch => "PATCH",
67 Self::Head => "HEAD",
68 Self::Options => "OPTIONS",
69 }
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, VariantNames)]
75#[serde(rename_all = "lowercase")]
76#[strum(serialize_all = "lowercase")]
77pub enum ResponseType {
78 Json,
80 Text,
82 Binary,
84}
85
86impl EnumVariants for ResponseType {
87 fn variant_names() -> &'static [&'static str] {
88 Self::VARIANTS
89 }
90}
91
92impl Default for ResponseType {
93 fn default() -> Self {
94 Self::Json
95 }
96}
97
98impl ResponseType {
99 pub fn as_str(&self) -> &str {
100 match self {
101 Self::Json => "json",
102 Self::Text => "text",
103 Self::Binary => "binary",
104 }
105 }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, Default)]
117#[serde(transparent)]
118pub struct HttpBody(pub Value);
119
120impl HttpBody {
121 pub fn is_empty(&self) -> bool {
123 self.0.is_null()
124 }
125
126 pub fn to_string_body(&self) -> Option<String> {
128 match &self.0 {
129 Value::Null => None,
130 Value::String(s) if s.is_empty() => None,
131 Value::String(s) => Some(s.clone()),
132 other => Some(other.to_string()),
133 }
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139#[serde(untagged)]
140pub enum HttpResponseBody {
141 #[serde(with = "base64_string")]
143 Binary(Vec<u8>),
144 Text(String),
146 Json(Value),
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, VariantNames)]
152#[serde(rename_all = "lowercase")]
153#[strum(serialize_all = "lowercase")]
154pub enum BodyType {
155 Json,
157 Text,
159 Binary,
161 Multipart,
163}
164
165impl EnumVariants for BodyType {
166 fn variant_names() -> &'static [&'static str] {
167 Self::VARIANTS
168 }
169}
170
171impl Default for BodyType {
172 fn default() -> Self {
173 Self::Json
174 }
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct MultipartPart {
180 pub name: String,
182
183 #[serde(flatten)]
185 pub content: MultipartContent,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(untagged)]
190pub enum MultipartContent {
191 Text { value: String },
193
194 File {
196 content: String,
198 #[serde(skip_serializing_if = "Option::is_none")]
200 filename: Option<String>,
201 #[serde(skip_serializing_if = "Option::is_none")]
203 #[serde(rename = "contentType")]
204 content_type: Option<String>,
205 },
206}
207
208#[derive(Debug, Serialize, Deserialize, CapabilityInput)]
210#[capability_input(display_name = "HTTP Request Input")]
211pub struct HttpRequestInput {
212 #[field(
214 display_name = "Method",
215 description = "HTTP verb for the request",
216 example = "GET",
217 default = "GET",
218 enum_type = "HttpMethod"
219 )]
220 #[serde(default)]
221 pub method: HttpMethod,
222
223 #[field(
225 display_name = "URL",
226 description = "Full URL to send the request to",
227 example = "https://api.example.com/v1/users"
228 )]
229 pub url: String,
230
231 #[field(
233 display_name = "Connection ID",
234 description = "Connection ID for automatic authentication and URL prefixing"
235 )]
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub connection_id: Option<String>,
238
239 #[field(
241 display_name = "Headers",
242 description = "Custom HTTP headers",
243 example = r#"{"Authorization": "Bearer token123"}"#,
244 default = "{}"
245 )]
246 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
247 pub headers: HashMap<String, String>,
248
249 #[field(
251 display_name = "Query Parameters",
252 description = "URL query parameters",
253 example = r#"{"page": "1", "limit": "100"}"#,
254 default = "{}"
255 )]
256 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
257 pub query_parameters: HashMap<String, String>,
258
259 #[field(
261 display_name = "Body",
262 description = "Request payload",
263 example = r#"{"name": "John Doe", "email": "john@example.com"}"#,
264 default = "null"
265 )]
266 #[serde(default, skip_serializing_if = "HttpBody::is_empty")]
267 pub body: HttpBody,
268
269 #[field(
271 display_name = "Body Type",
272 description = "How to encode the request body",
273 example = "json",
274 default = "json",
275 enum_type = "BodyType"
276 )]
277 #[serde(default)]
278 pub body_type: BodyType,
279
280 #[field(
282 display_name = "Multipart Parts",
283 description = "Form fields and files to include in multipart requests",
284 default = "[]"
285 )]
286 #[serde(default, skip_serializing_if = "Vec::is_empty")]
287 pub multipart: Vec<MultipartPart>,
288
289 #[field(
291 display_name = "Response Type",
292 description = "Expected response format",
293 example = "json",
294 default = "json",
295 enum_type = "ResponseType"
296 )]
297 #[serde(default)]
298 pub response_type: ResponseType,
299
300 #[field(
302 display_name = "Timeout (ms)",
303 description = "Maximum time to wait for response",
304 example = "5000",
305 default = "30000"
306 )]
307 #[serde(default = "default_timeout")]
308 pub timeout_ms: u64,
309
310 #[field(
312 display_name = "Fail on Error",
313 description = "If true (default), non-2xx responses will fail the step. If false, non-2xx responses are returned normally.",
314 example = "true",
315 default = "true"
316 )]
317 #[serde(default = "default_fail_on_error")]
318 pub fail_on_error: bool,
319}
320
321impl Default for HttpRequestInput {
322 fn default() -> Self {
323 HttpRequestInput {
324 method: HttpMethod::default(),
325 url: String::new(),
326 connection_id: None,
327 headers: HashMap::new(),
328 query_parameters: HashMap::new(),
329 body: HttpBody(Value::Null),
330 response_type: ResponseType::default(),
331 timeout_ms: default_timeout(),
332 body_type: BodyType::default(),
333 multipart: Vec::new(),
334 fail_on_error: default_fail_on_error(),
335 }
336 }
337}
338
339fn default_timeout() -> u64 {
340 30000
341}
342
343fn default_fail_on_error() -> bool {
344 true
345}
346
347#[derive(Debug, Serialize, Deserialize)]
349#[allow(dead_code)]
350struct HttpResponseMetadata {
351 pub status_code: u16,
353
354 pub headers: HashMap<String, String>,
356
357 pub body_length: usize,
359
360 pub response_type: String,
362
363 pub success: bool,
365}
366
367#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
369#[capability_output(
370 display_name = "HTTP Response",
371 description = "Response from an HTTP request"
372)]
373pub struct HttpResponse {
374 #[field(
375 display_name = "Status Code",
376 description = "HTTP status code (e.g., 200, 404, 500)",
377 example = "200"
378 )]
379 pub status_code: u16,
380
381 #[field(
382 display_name = "Headers",
383 description = "Response headers as key-value pairs"
384 )]
385 pub headers: HashMap<String, String>,
386
387 #[field(
388 display_name = "Body",
389 description = "Response body (JSON object, text string, or base64-encoded binary depending on response_type)"
390 )]
391 pub body: HttpResponseBody,
392
393 #[field(
394 display_name = "Success",
395 description = "True if the status code is in the 2xx range",
396 example = "true"
397 )]
398 pub success: bool,
399}
400
401pub use crate::extractors::HttpConnectionConfig;
403
404pub fn extract_connection_config(
406 raw: &crate::connections::RawConnection,
407) -> Result<HttpConnectionConfig, String> {
408 crate::extractors::extract_http_config(
409 &raw.integration_id,
410 &raw.parameters,
411 raw.rate_limit_config.clone(),
412 )
413}
414
415#[capability(
421 module = "http",
422 display_name = "HTTP Request",
423 description = "Execute an HTTP request with the specified method, URL, headers, and body",
424 side_effects = true
425)]
426pub fn http_request(input: HttpRequestInput) -> Result<HttpResponse, String> {
427 let mut headers = input.headers.clone();
429 let mut query_parameters = input.query_parameters.clone();
430 let mut url = input.url.clone();
431
432 if let Some(ref conn_id) = input.connection_id {
434 let raw = crate::connections::resolve_connection(conn_id)?;
435 let config = extract_connection_config(&raw)?;
436
437 if !url.starts_with("http://") && !url.starts_with("https://") {
439 url = format!("{}{}", config.url_prefix, url);
440 }
441
442 for (k, v) in config.headers {
444 headers.entry(k).or_insert(v);
445 }
446
447 for (k, v) in config.query_parameters {
449 query_parameters.entry(k).or_insert(v);
450 }
451
452 }
454
455 if !query_parameters.is_empty() {
457 let query_string: String = query_parameters
458 .iter()
459 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
460 .collect::<Vec<_>>()
461 .join("&");
462
463 if url.contains('?') {
464 url = format!("{}&{}", url, query_string);
465 } else {
466 url = format!("{}?{}", url, query_string);
467 }
468 }
469
470 let agent = ureq::AgentBuilder::new()
472 .timeout(std::time::Duration::from_millis(input.timeout_ms))
473 .build();
474
475 let request = match input.method {
477 HttpMethod::Get => agent.get(&url),
478 HttpMethod::Post => agent.post(&url),
479 HttpMethod::Put => agent.put(&url),
480 HttpMethod::Delete => agent.delete(&url),
481 HttpMethod::Patch => agent.patch(&url),
482 HttpMethod::Head => agent.head(&url),
483 HttpMethod::Options => agent.request("OPTIONS", &url),
484 };
485
486 let mut request = request;
488 for (key, value) in &headers {
489 request = request.set(key, value);
490 }
491
492 let response = match input.method {
494 HttpMethod::Get | HttpMethod::Head | HttpMethod::Options | HttpMethod::Delete => {
495 request.call()
496 }
497 HttpMethod::Post | HttpMethod::Put | HttpMethod::Patch => {
498 if let Some(body_str) = input.body.to_string_body() {
499 if !headers.contains_key("Content-Type") && !headers.contains_key("content-type") {
501 request = request.set("Content-Type", "application/json");
502 }
503 request.send_string(&body_str)
504 } else {
505 request.call()
506 }
507 }
508 };
509
510 match response {
512 Ok(resp) => {
513 let status_code = resp.status();
514
515 let mut headers = HashMap::new();
517 for name in resp.headers_names() {
518 if let Some(value) = resp.header(&name) {
519 headers.insert(name, value.to_string());
520 }
521 }
522
523 let body = match input.response_type {
525 ResponseType::Json => match resp.into_string() {
526 Ok(text) => match serde_json::from_str(&text) {
527 Ok(json_value) => HttpResponseBody::Json(json_value),
528 Err(_) => HttpResponseBody::Text(text),
529 },
530 Err(e) => return Err(format!("Failed to read response body: {}", e)),
531 },
532 ResponseType::Text => match resp.into_string() {
533 Ok(text) => HttpResponseBody::Text(text),
534 Err(e) => return Err(format!("Failed to read response body: {}", e)),
535 },
536 ResponseType::Binary => {
537 let mut bytes = Vec::new();
538 match resp.into_reader().read_to_end(&mut bytes) {
539 Ok(_) => HttpResponseBody::Binary(bytes),
540 Err(e) => return Err(format!("Failed to read response body: {}", e)),
541 }
542 }
543 };
544
545 let success = status_code >= 200 && status_code < 300;
546
547 Ok(HttpResponse {
548 status_code,
549 headers,
550 body,
551 success,
552 })
553 }
554 Err(ureq::Error::Status(status_code, resp)) => {
555 let mut headers = HashMap::new();
557 for name in resp.headers_names() {
558 if let Some(value) = resp.header(&name) {
559 headers.insert(name, value.to_string());
560 }
561 }
562
563 let body_text = resp.into_string().unwrap_or_default();
564 let body = match serde_json::from_str(&body_text) {
565 Ok(json_value) => HttpResponseBody::Json(json_value),
566 Err(_) => HttpResponseBody::Text(body_text.clone()),
567 };
568
569 if input.fail_on_error {
571 return Err(format!(
572 "HTTP request failed with status {}: {}",
573 status_code, body_text
574 ));
575 }
576
577 Ok(HttpResponse {
578 status_code,
579 headers,
580 body,
581 success: false,
582 })
583 }
584 Err(ureq::Error::Transport(transport)) => Err(format!(
585 "HTTP request to {} failed: {}",
586 input.url, transport
587 )),
588 }
589}
590
591mod urlencoding {
593 pub fn encode(s: &str) -> String {
594 let mut result = String::new();
595 for c in s.chars() {
596 match c {
597 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '~' => result.push(c),
598 _ => {
599 for byte in c.to_string().as_bytes() {
600 result.push_str(&format!("%{:02X}", byte));
601 }
602 }
603 }
604 }
605 result
606 }
607}
608
609mod base64_string {
610 use base64::{Engine as _, engine::general_purpose};
611 use serde::{Deserialize, Deserializer, Serializer};
612
613 pub fn serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
614 where
615 S: Serializer,
616 {
617 let encoded = general_purpose::STANDARD.encode(bytes);
618 serializer.serialize_str(&encoded)
619 }
620
621 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
622 where
623 D: Deserializer<'de>,
624 {
625 let encoded = String::deserialize(deserializer)?;
626 general_purpose::STANDARD
627 .decode(encoded.as_bytes())
628 .map_err(serde::de::Error::custom)
629 }
630}