proxy_sdk/
http_call.rs

1use std::{
2    ops::{Bound, RangeBounds},
3    time::Duration,
4};
5
6use derive_builder::Builder;
7
8use crate::{
9    downcast_box::DowncastBox,
10    hostcalls::{self, BufferType, MapType},
11    log_concern,
12    upstream::Upstream,
13    RootContext, Status,
14};
15
16/// Outbound HTTP call
17#[derive(Builder)]
18#[builder(setter(into))]
19#[builder(pattern = "owned")]
20#[allow(clippy::type_complexity)]
21pub struct HttpCall<'a> {
22    /// Upstream cluster to send the request to.
23    pub upstream: Upstream<'a>,
24    /// All headers to be sent along with the request. The proxy may add additional headers.
25    /// This should include pseudo headers like `:method` and `:path`.
26    #[builder(setter(into, each(name = "header")), default)]
27    pub headers: Vec<(&'a str, &'a [u8])>,
28    /// All trailers to be sent along with the request.
29    #[builder(setter(into, each(name = "trailer")), default)]
30    pub trailers: Vec<(&'a str, &'a [u8])>,
31    /// An optional request body to send with the request.
32    #[builder(setter(strip_option, into), default)]
33    pub body: Option<&'a [u8]>,
34    /// A timeout on waiting for a response. Default is 10 seconds.
35    #[builder(setter(strip_option, into), default)]
36    pub timeout: Option<Duration>,
37    /// Callback to call when a response has arrived.
38    #[builder(setter(custom), default)]
39    pub callback: Option<Box<dyn FnOnce(&mut DowncastBox<dyn RootContext>, &HttpCallResponse)>>,
40}
41
42impl<'a> HttpCallBuilder<'a> {
43    /// Set a response callback
44    pub fn callback<R: RootContext + 'static>(
45        mut self,
46        callback: impl FnOnce(&mut R, &HttpCallResponse) + 'static,
47    ) -> Self {
48        self.callback = Some(Some(Box::new(move |root, resp| {
49            callback(
50                root.as_any_mut().downcast_mut().expect("invalid root type"),
51                resp,
52            )
53        })));
54        self
55    }
56}
57
58impl<'a> HttpCall<'a> {
59    const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
60
61    /// Sends this `HttpCall` over the network.
62    pub fn dispatch(self) -> Result<(), Status> {
63        let token = hostcalls::dispatch_http_call(
64            &self.upstream.0,
65            &self.headers,
66            self.body,
67            &self.trailers,
68            self.timeout.unwrap_or(Self::DEFAULT_TIMEOUT),
69        )?;
70        if let Some(callback) = self.callback {
71            crate::dispatcher::register_http_callback(token, callback);
72        }
73        Ok(())
74    }
75}
76
77/// Response type for [`HttpCall::callback`]
78pub struct HttpCallResponse {
79    num_headers: usize,
80    body_size: usize,
81    num_trailers: usize,
82}
83
84impl HttpCallResponse {
85    pub(crate) fn new(num_headers: usize, body_size: usize, num_trailers: usize) -> Self {
86        Self {
87            num_headers,
88            body_size,
89            num_trailers,
90        }
91    }
92
93    /// Number of headers contained
94    pub fn num_headers(&self) -> usize {
95        self.num_headers
96    }
97
98    /// Number of trailers contained
99    pub fn num_trailers(&self) -> usize {
100        self.num_trailers
101    }
102
103    /// Total size of the response body
104    pub fn body_size(&self) -> usize {
105        self.body_size
106    }
107
108    /// Get all response headers
109    pub fn headers(&self) -> Vec<(String, Vec<u8>)> {
110        log_concern(
111            "http-call-headers",
112            hostcalls::get_map(MapType::HttpCallResponseHeaders),
113        )
114        .unwrap_or_default()
115    }
116
117    /// Get a specific response header
118    pub fn header(&self, name: impl AsRef<str>) -> Option<Vec<u8>> {
119        log_concern(
120            "http-call-header",
121            hostcalls::get_map_value(MapType::HttpCallResponseHeaders, name.as_ref()),
122        )
123    }
124
125    /// Get a range of the response body
126    pub fn body(&self, range: impl RangeBounds<usize>) -> Option<Vec<u8>> {
127        let start = match range.start_bound() {
128            Bound::Included(x) => *x,
129            Bound::Excluded(x) => x.saturating_sub(1),
130            Bound::Unbounded => 0,
131        };
132        let size = match range.end_bound() {
133            Bound::Included(x) => *x + 1,
134            Bound::Excluded(x) => *x,
135            Bound::Unbounded => self.body_size,
136        }
137        .min(self.body_size)
138        .saturating_sub(start);
139
140        log_concern(
141            "http-call-body",
142            hostcalls::get_buffer(BufferType::HttpCallResponseBody, start, size),
143        )
144    }
145
146    /// Get the entire response body
147    pub fn full_body(&self) -> Option<Vec<u8>> {
148        self.body(..)
149    }
150
151    /// Get all response trailers
152    pub fn trailers(&self) -> Vec<(String, Vec<u8>)> {
153        log_concern(
154            "http-call-trailers",
155            hostcalls::get_map(MapType::HttpCallResponseTrailers),
156        )
157        .unwrap_or_default()
158    }
159
160    /// Get a specific response trailer
161    pub fn trailer(&self, name: impl AsRef<str>) -> Option<Vec<u8>> {
162        log_concern(
163            "http-call-trailer",
164            hostcalls::get_map_value(MapType::HttpCallResponseTrailers, name.as_ref()),
165        )
166    }
167}