Skip to main content

cloud_sdk/transport/
raw.rs

1//! Provider-neutral raw HTTP execution and response-wire policy.
2
3use core::future::Future;
4
5use super::{HeaderName, MediaType, ResponseWriter, StatusCode, TransportRequest};
6
7/// Maximum informational response heads accepted before a final response.
8pub const MAX_INFORMATIONAL_RESPONSES: u8 = 8;
9/// Maximum chunks admitted by one buffered raw response.
10pub const MAX_RESPONSE_CHUNKS: usize = 4_096;
11/// Maximum per-class body limit represented by the raw buffered contract.
12pub const MAX_RAW_RESPONSE_BODY_BYTES: usize = 64 * 1024 * 1024;
13
14/// Response media-type requirement for one status class.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum ResponseMediaPolicy<'a> {
17    /// A content type is required and must match one admitted essence.
18    Required(&'a [MediaType<'a>]),
19    /// A content type may be absent, but a present value must match.
20    Optional(&'a [MediaType<'a>]),
21    /// A content type and response body are forbidden.
22    Forbidden,
23}
24
25/// Response trailer handling.
26#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
27pub enum TrailerPolicy {
28    /// Reject declared or observed response trailers.
29    Reject,
30}
31
32/// Invalid raw response-wire policy.
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum RawResponsePolicyError {
35    /// An informational-response limit exceeded the global hard ceiling.
36    InformationalLimitTooLarge,
37    /// A body limit exceeded the buffered raw-response ceiling.
38    BodyLimitTooLarge,
39    /// Required or optional media policy omitted accepted media types.
40    MissingMediaType,
41    /// A media policy contains duplicate media-type essences.
42    DuplicateMediaType,
43    /// Too many response-header names were admitted.
44    TooManyAdmittedHeaders,
45    /// Admitted response-header names contain a duplicate.
46    DuplicateAdmittedHeader,
47    /// Credential, cookie, framing, proxy, or upgrade response metadata was admitted.
48    UnsafeAdmittedHeader,
49    /// A status class forbids media and body data but has a nonzero body limit.
50    ForbiddenMediaHasBodyLimit,
51}
52
53impl_static_error!(RawResponsePolicyError,
54    Self::InformationalLimitTooLarge => "informational response limit is too large",
55    Self::BodyLimitTooLarge => "raw response body limit is too large",
56    Self::MissingMediaType => "response media policy has no accepted media type",
57    Self::DuplicateMediaType => "response media policy contains duplicate media types",
58    Self::TooManyAdmittedHeaders => "too many response headers are admitted",
59    Self::DuplicateAdmittedHeader => "an admitted response header is duplicated",
60    Self::UnsafeAdmittedHeader => "an unsafe response header was admitted",
61    Self::ForbiddenMediaHasBodyLimit => "forbidden response media has a nonzero body limit",
62);
63
64/// Complete provider-neutral admission policy for one final HTTP response.
65#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub struct RawResponsePolicy<'a> {
67    success_body_bytes: usize,
68    error_body_bytes: usize,
69    success_media: ResponseMediaPolicy<'a>,
70    error_media: ResponseMediaPolicy<'a>,
71    admitted_headers: &'a [HeaderName<'a>],
72    informational_limit: u8,
73    trailer_policy: TrailerPolicy,
74}
75
76impl<'a> RawResponsePolicy<'a> {
77    /// Validates separate success/error bounds, media rules, and retained headers.
78    pub fn new(
79        success_body_bytes: usize,
80        error_body_bytes: usize,
81        success_media: ResponseMediaPolicy<'a>,
82        error_media: ResponseMediaPolicy<'a>,
83        admitted_headers: &'a [HeaderName<'a>],
84        informational_limit: u8,
85    ) -> Result<Self, RawResponsePolicyError> {
86        if success_body_bytes > MAX_RAW_RESPONSE_BODY_BYTES
87            || error_body_bytes > MAX_RAW_RESPONSE_BODY_BYTES
88        {
89            return Err(RawResponsePolicyError::BodyLimitTooLarge);
90        }
91        if informational_limit > MAX_INFORMATIONAL_RESPONSES {
92            return Err(RawResponsePolicyError::InformationalLimitTooLarge);
93        }
94        validate_media(success_media)?;
95        validate_media(error_media)?;
96        if (matches!(success_media, ResponseMediaPolicy::Forbidden) && success_body_bytes != 0)
97            || (matches!(error_media, ResponseMediaPolicy::Forbidden) && error_body_bytes != 0)
98        {
99            return Err(RawResponsePolicyError::ForbiddenMediaHasBodyLimit);
100        }
101        validate_headers(admitted_headers)?;
102        Ok(Self {
103            success_body_bytes,
104            error_body_bytes,
105            success_media,
106            error_media,
107            admitted_headers,
108            informational_limit,
109            trailer_policy: TrailerPolicy::Reject,
110        })
111    }
112
113    /// Returns the body limit selected by the final status class.
114    #[must_use]
115    pub const fn body_limit(self, status: StatusCode) -> usize {
116        if status.is_success() {
117            self.success_body_bytes
118        } else {
119            self.error_body_bytes
120        }
121    }
122
123    /// Returns the media policy selected by the final status class.
124    #[must_use]
125    pub const fn media_policy(self, status: StatusCode) -> ResponseMediaPolicy<'a> {
126        if status.is_success() {
127            self.success_media
128        } else {
129            self.error_media
130        }
131    }
132
133    /// Reports whether an operation admitted retention of this response header.
134    #[must_use]
135    pub fn admits_header(self, name: &str) -> bool {
136        self.admitted_headers
137            .iter()
138            .any(|candidate| candidate.eq_ignore_ascii_case(name))
139    }
140
141    /// Returns the informational-response limit.
142    #[must_use]
143    pub const fn informational_limit(self) -> u8 {
144        self.informational_limit
145    }
146
147    /// Returns the explicit trailer policy.
148    #[must_use]
149    pub const fn trailer_policy(self) -> TrailerPolicy {
150        self.trailer_policy
151    }
152}
153
154/// Informational or final-response selection failure.
155#[derive(Clone, Copy, Debug, Eq, PartialEq)]
156pub enum InformationalResponseError {
157    /// `101 Switching Protocols` is never admitted by buffered cloud requests.
158    SwitchingProtocols,
159    /// Too many informational responses preceded the final response.
160    TooManyInformationalResponses,
161    /// A final response was still informational.
162    MissingFinalResponse,
163}
164
165impl_static_error!(InformationalResponseError,
166    Self::SwitchingProtocols => "switching protocols is forbidden",
167    Self::TooManyInformationalResponses => "too many informational responses",
168    Self::MissingFinalResponse => "final HTTP response is missing",
169);
170
171/// Small state machine for bounded informational-response handling.
172#[derive(Clone, Copy, Debug, Eq, PartialEq)]
173pub struct InformationalResponseTracker {
174    limit: u8,
175    observed: u8,
176}
177
178impl InformationalResponseTracker {
179    /// Creates a tracker from a validated response policy.
180    #[must_use]
181    pub const fn new(policy: RawResponsePolicy<'_>) -> Self {
182        Self {
183            limit: policy.informational_limit,
184            observed: 0,
185        }
186    }
187
188    /// Observes one response head and reports whether it is the final head.
189    pub fn observe(&mut self, status: StatusCode) -> Result<bool, InformationalResponseError> {
190        if status.get() == 101 {
191            return Err(InformationalResponseError::SwitchingProtocols);
192        }
193        if status.get() < 200 {
194            self.observed = self
195                .observed
196                .checked_add(1)
197                .ok_or(InformationalResponseError::TooManyInformationalResponses)?;
198            if self.observed > self.limit {
199                return Err(InformationalResponseError::TooManyInformationalResponses);
200            }
201            return Ok(false);
202        }
203        Ok(true)
204    }
205
206    /// Returns the number of admitted informational heads.
207    #[must_use]
208    pub const fn observed(self) -> u8 {
209        self.observed
210    }
211}
212
213/// Blocking execution of one already validated raw HTTP request.
214pub trait BlockingRawHttpExecutor {
215    /// Executor-specific phased failure.
216    type Error;
217
218    /// Executes exactly once without implicit authentication or retries.
219    ///
220    /// Implementations must use [`ResponseWriter::begin_attempt`]. Response
221    /// mutation and commitment are available only through the returned guard.
222    fn execute(
223        &self,
224        request: TransportRequest<'_>,
225        policy: RawResponsePolicy<'_>,
226        response: &mut ResponseWriter<'_>,
227    ) -> Result<(), Self::Error>;
228}
229
230/// Runtime-neutral asynchronous raw HTTP execution.
231pub trait AsyncRawHttpExecutor {
232    /// Executor-specific phased failure.
233    type Error;
234
235    /// Executes exactly once without implicit authentication or retries.
236    ///
237    /// Implementations must use [`ResponseWriter::begin_attempt`]. Response
238    /// mutation and commitment are available only through the returned guard,
239    /// which clears state across future cancellation.
240    fn execute<'executor, 'request, 'policy, 'writer>(
241        &'executor self,
242        request: TransportRequest<'request>,
243        policy: RawResponsePolicy<'policy>,
244        response: &'writer mut ResponseWriter<'_>,
245    ) -> impl Future<Output = Result<(), Self::Error>> + Send + 'writer
246    where
247        'executor: 'writer,
248        'request: 'writer,
249        'policy: 'writer;
250}
251
252fn validate_media(policy: ResponseMediaPolicy<'_>) -> Result<(), RawResponsePolicyError> {
253    let media = match policy {
254        ResponseMediaPolicy::Required(media) | ResponseMediaPolicy::Optional(media) => media,
255        ResponseMediaPolicy::Forbidden => return Ok(()),
256    };
257    if media.is_empty() {
258        return Err(RawResponsePolicyError::MissingMediaType);
259    }
260    for (index, value) in media.iter().enumerate() {
261        if media.get(..index).is_some_and(|seen| {
262            seen.iter()
263                .any(|candidate| candidate.as_str().eq_ignore_ascii_case(value.as_str()))
264        }) {
265            return Err(RawResponsePolicyError::DuplicateMediaType);
266        }
267    }
268    Ok(())
269}
270
271fn validate_headers(headers: &[HeaderName<'_>]) -> Result<(), RawResponsePolicyError> {
272    if headers.len() > super::MAX_RESPONSE_HEADERS {
273        return Err(RawResponsePolicyError::TooManyAdmittedHeaders);
274    }
275    for (index, header) in headers.iter().enumerate() {
276        if is_unsafe_response_header(header.as_str()) {
277            return Err(RawResponsePolicyError::UnsafeAdmittedHeader);
278        }
279        if headers.get(..index).is_some_and(|seen| {
280            seen.iter()
281                .any(|candidate| candidate.eq_ignore_ascii_case(header.as_str()))
282        }) {
283            return Err(RawResponsePolicyError::DuplicateAdmittedHeader);
284        }
285    }
286    Ok(())
287}
288
289fn is_unsafe_response_header(name: &str) -> bool {
290    [
291        "authorization",
292        "connection",
293        "cookie",
294        "proxy-authenticate",
295        "proxy-authorization",
296        "set-cookie",
297        "te",
298        "trailer",
299        "transfer-encoding",
300        "upgrade",
301    ]
302    .iter()
303    .any(|candidate| name.eq_ignore_ascii_case(candidate))
304}
305
306#[cfg(test)]
307mod tests {
308    use super::{
309        InformationalResponseError, InformationalResponseTracker, RawResponsePolicy,
310        RawResponsePolicyError, ResponseMediaPolicy,
311    };
312    use crate::transport::{HeaderName, MediaType, StatusCode};
313
314    fn policy(limit: u8) -> Result<RawResponsePolicy<'static>, RawResponsePolicyError> {
315        RawResponsePolicy::new(
316            1024,
317            256,
318            ResponseMediaPolicy::Required(&[MediaType::JSON]),
319            ResponseMediaPolicy::Optional(&[MediaType::JSON]),
320            &[],
321            limit,
322        )
323    }
324
325    #[test]
326    fn selects_independent_success_and_error_limits() {
327        let Ok(policy) = policy(2) else {
328            return;
329        };
330        assert_eq!(policy.body_limit(StatusCode::OK), 1024);
331        assert_eq!(
332            policy.body_limit(StatusCode::new(400).unwrap_or(StatusCode::TOO_MANY_REQUESTS)),
333            256
334        );
335    }
336
337    #[test]
338    fn bounds_informationals_and_rejects_switching_protocols() {
339        let Ok(policy) = policy(2) else {
340            return;
341        };
342        let mut tracker = InformationalResponseTracker::new(policy);
343        let early = StatusCode::new(103).unwrap_or(StatusCode::OK);
344        assert_eq!(tracker.observe(early), Ok(false));
345        assert_eq!(tracker.observe(early), Ok(false));
346        assert_eq!(
347            tracker.observe(early),
348            Err(InformationalResponseError::TooManyInformationalResponses)
349        );
350        let switching = StatusCode::new(101).unwrap_or(StatusCode::OK);
351        assert_eq!(
352            InformationalResponseTracker::new(policy).observe(switching),
353            Err(InformationalResponseError::SwitchingProtocols)
354        );
355    }
356
357    #[test]
358    fn rejects_unsafe_and_duplicate_admitted_headers() {
359        let unsafe_header = HeaderName::new("set-cookie");
360        assert!(unsafe_header.is_ok());
361        if let Ok(unsafe_header) = unsafe_header {
362            assert!(matches!(
363                RawResponsePolicy::new(
364                    1,
365                    1,
366                    ResponseMediaPolicy::Optional(&[MediaType::JSON]),
367                    ResponseMediaPolicy::Optional(&[MediaType::JSON]),
368                    &[unsafe_header],
369                    0,
370                ),
371                Err(RawResponsePolicyError::UnsafeAdmittedHeader)
372            ));
373        }
374        let first = HeaderName::new("x-request-id");
375        let second = HeaderName::new("X-Request-ID");
376        assert!(first.is_ok() && second.is_ok());
377        if let (Ok(first), Ok(second)) = (first, second) {
378            assert!(matches!(
379                RawResponsePolicy::new(
380                    1,
381                    1,
382                    ResponseMediaPolicy::Optional(&[MediaType::JSON]),
383                    ResponseMediaPolicy::Optional(&[MediaType::JSON]),
384                    &[first, second],
385                    0,
386                ),
387                Err(RawResponsePolicyError::DuplicateAdmittedHeader)
388            ));
389        }
390    }
391}