Skip to main content

acp_runtime/
transport.rs

1// Copyright 2026 ACP Project
2// Licensed under the Apache License, Version 2.0
3// See LICENSE file for details.
4
5use std::time::Duration;
6
7use serde_json::{Map, Value};
8
9use crate::errors::{AcpError, AcpResult};
10use crate::http_security::{HttpSecurityPolicy, build_http_client, validate_http_url};
11use crate::messages::AcpMessage;
12
13#[derive(Debug, Clone)]
14pub struct TransportResponse {
15    pub status_code: u16,
16    pub body: Option<Map<String, Value>>,
17    pub raw_body: String,
18}
19
20#[derive(Debug, Clone)]
21pub struct TransportClient {
22    client: reqwest::blocking::Client,
23    timeout_seconds: u64,
24    allow_insecure_http: bool,
25    mtls_enabled: bool,
26}
27
28impl TransportClient {
29    pub fn new(timeout_seconds: u64, policy: &HttpSecurityPolicy) -> AcpResult<Self> {
30        let client = build_http_client(timeout_seconds.max(1), policy)?;
31        Ok(Self {
32            client,
33            timeout_seconds: timeout_seconds.max(1),
34            allow_insecure_http: policy.allow_insecure_http,
35            mtls_enabled: policy.mtls_enabled,
36        })
37    }
38
39    pub fn post_json(&self, url: &str, body: &Map<String, Value>) -> AcpResult<TransportResponse> {
40        validate_http_url(
41            url,
42            self.allow_insecure_http,
43            self.mtls_enabled,
44            "HTTP transport request",
45        )?;
46        let response = self
47            .client
48            .post(url)
49            .header("Content-Type", "application/json")
50            .timeout(Duration::from_secs(self.timeout_seconds))
51            .json(body)
52            .send()?;
53        let status_code = response.status().as_u16();
54        let raw_body = response.text().unwrap_or_default();
55        let body = serde_json::from_str::<Value>(&raw_body)
56            .ok()
57            .and_then(|value| value.as_object().cloned());
58        Ok(TransportResponse {
59            status_code,
60            body,
61            raw_body,
62        })
63    }
64
65    pub fn send_to_relay(
66        &self,
67        relay_url: &str,
68        message: &AcpMessage,
69    ) -> AcpResult<Map<String, Value>> {
70        let relay_endpoint = if relay_url.ends_with('/') {
71            format!("{relay_url}messages")
72        } else {
73            format!("{relay_url}/messages")
74        };
75        let response = self.post_json(&relay_endpoint, &message.to_map()?)?;
76        if response.status_code != 200 {
77            return Err(AcpError::Transport(format!(
78                "Relay returned HTTP {} for message {}",
79                response.status_code, message.envelope.message_id
80            )));
81        }
82        response
83            .body
84            .ok_or_else(|| AcpError::Transport("Relay returned non-JSON response".to_string()))
85    }
86}