Skip to main content

trql_client/
https.rs

1//! HTTPS binding: `POST <registry>/trust-tasks` per the `trust-tasks-https`
2//! wire contract (JSON body = the request document; a 2xx body is the reply
3//! document, a non-2xx body is a `trust-task-error` document).
4//!
5//! Implemented directly on `reqwest` rather than `trust_tasks_https::HttpsClient`:
6//! that client's typed `send` bounds (`Payload` on both sides) don't admit the
7//! untyped `TrustTask<Value>` seam this crate routes every binding through.
8//! (Its historical lack of timeouts was fixed upstream in trust-tasks-https
9//! 0.2.1.) The wire contract is identical and is pinned by the round-trip
10//! test against an in-process server.
11
12use std::time::Duration;
13
14use serde_json::Value;
15use trust_tasks_rs::TrustTask;
16
17use crate::error::TrqlError;
18use crate::transport::{TransportKind, TrqlTransport};
19
20/// Configuration for [`HttpsTransport`].
21#[derive(Debug, Clone)]
22pub struct HttpsTransportConfig {
23    /// Base URL of the registry, e.g. `https://registry.example.com` — the
24    /// transport POSTs to `<base>/trust-tasks`.
25    pub base_url: String,
26    /// End-to-end request timeout.
27    pub timeout: Duration,
28    /// Connection-establishment timeout.
29    pub connect_timeout: Duration,
30    /// Optional bearer token (`Authorization: Bearer <token>`).
31    pub bearer_token: Option<String>,
32}
33
34impl HttpsTransportConfig {
35    /// Defaults: 30s request timeout, 10s connect timeout, no bearer token.
36    pub fn new(base_url: impl Into<String>) -> Self {
37        Self {
38            base_url: base_url.into(),
39            timeout: Duration::from_secs(30),
40            connect_timeout: Duration::from_secs(10),
41            bearer_token: None,
42        }
43    }
44}
45
46/// [`TrqlTransport`] over the `trust-tasks-https` binding.
47///
48/// The inner `reqwest::Client` is built once with finite timeouts and reused
49/// for every exchange.
50pub struct HttpsTransport {
51    http: reqwest::Client,
52    endpoint: reqwest::Url,
53    bearer_token: Option<String>,
54    timeout: Duration,
55}
56
57impl HttpsTransport {
58    /// Build the transport, validating the URL and constructing the shared
59    /// HTTP client.
60    pub fn new(config: HttpsTransportConfig) -> Result<Self, TrqlError> {
61        let endpoint: reqwest::Url =
62            format!("{}/trust-tasks", config.base_url.trim_end_matches('/'))
63                .parse()
64                .map_err(|e| TrqlError::Config(format!("invalid registry base URL: {e}")))?;
65        let http = reqwest::Client::builder()
66            .timeout(config.timeout)
67            .connect_timeout(config.connect_timeout)
68            .build()
69            .map_err(|e| TrqlError::Config(format!("could not build HTTP client: {e}")))?;
70        Ok(Self {
71            http,
72            endpoint,
73            bearer_token: config.bearer_token,
74            timeout: config.timeout,
75        })
76    }
77}
78
79#[async_trait::async_trait]
80impl TrqlTransport for HttpsTransport {
81    fn kind(&self) -> TransportKind {
82        TransportKind::Https
83    }
84
85    async fn exchange(&self, request: TrustTask<Value>) -> Result<TrustTask<Value>, TrqlError> {
86        let mut http_request = self.http.post(self.endpoint.clone()).json(&request);
87        if let Some(token) = &self.bearer_token {
88            http_request = http_request.bearer_auth(token);
89        }
90
91        let response = http_request.send().await.map_err(|e| {
92            if e.is_timeout() {
93                TrqlError::Timeout {
94                    kind: TransportKind::Https,
95                    waited_secs: self.timeout.as_secs(),
96                }
97            } else if e.is_connect() {
98                TrqlError::Transport {
99                    kind: TransportKind::Https,
100                    detail: format!("could not connect to {}: {e}", self.endpoint),
101                }
102            } else {
103                TrqlError::Transport {
104                    kind: TransportKind::Https,
105                    detail: e.to_string(),
106                }
107            }
108        })?;
109
110        let status = response.status();
111        let body = response.bytes().await.map_err(|e| TrqlError::Transport {
112            kind: TransportKind::Https,
113            detail: format!("failed reading response body: {e}"),
114        })?;
115
116        // Success and error statuses both carry a Trust Task document (the
117        // error status carries `trust-task-error`); the client layer maps it.
118        match serde_json::from_slice::<TrustTask<Value>>(&body) {
119            Ok(document) => Ok(document),
120            Err(e) if status.is_success() => Err(TrqlError::Contract(format!(
121                "HTTP {status} body is not a Trust Task document: {e}"
122            ))),
123            Err(_) => Err(TrqlError::Transport {
124                kind: TransportKind::Https,
125                detail: format!(
126                    "HTTP {status} with non-Trust-Task body: {}",
127                    String::from_utf8_lossy(&body)
128                ),
129            }),
130        }
131    }
132}