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: [Option<HeaderName<'a>>; super::MAX_RESPONSE_HEADERS],
72    admitted_header_count: usize,
73    informational_limit: u8,
74    trailer_policy: TrailerPolicy,
75}
76
77impl<'a> RawResponsePolicy<'a> {
78    /// Validates separate success/error bounds, media rules, and retained headers.
79    pub fn new(
80        success_body_bytes: usize,
81        error_body_bytes: usize,
82        success_media: ResponseMediaPolicy<'a>,
83        error_media: ResponseMediaPolicy<'a>,
84        admitted_headers: &[HeaderName<'a>],
85        informational_limit: u8,
86    ) -> Result<Self, RawResponsePolicyError> {
87        if success_body_bytes > MAX_RAW_RESPONSE_BODY_BYTES
88            || error_body_bytes > MAX_RAW_RESPONSE_BODY_BYTES
89        {
90            return Err(RawResponsePolicyError::BodyLimitTooLarge);
91        }
92        if informational_limit > MAX_INFORMATIONAL_RESPONSES {
93            return Err(RawResponsePolicyError::InformationalLimitTooLarge);
94        }
95        validate_media(success_media)?;
96        validate_media(error_media)?;
97        if (matches!(success_media, ResponseMediaPolicy::Forbidden) && success_body_bytes != 0)
98            || (matches!(error_media, ResponseMediaPolicy::Forbidden) && error_body_bytes != 0)
99        {
100            return Err(RawResponsePolicyError::ForbiddenMediaHasBodyLimit);
101        }
102        validate_headers(admitted_headers)?;
103        let mut owned_headers = [None; super::MAX_RESPONSE_HEADERS];
104        for (index, header) in admitted_headers.iter().copied().enumerate() {
105            if let Some(slot) = owned_headers.get_mut(index) {
106                *slot = Some(header);
107            }
108        }
109        Ok(Self {
110            success_body_bytes,
111            error_body_bytes,
112            success_media,
113            error_media,
114            admitted_headers: owned_headers,
115            admitted_header_count: admitted_headers.len(),
116            informational_limit,
117            trailer_policy: TrailerPolicy::Reject,
118        })
119    }
120
121    /// Returns the body limit selected by the final status class.
122    #[must_use]
123    pub const fn body_limit(self, status: StatusCode) -> usize {
124        if status.is_success() {
125            self.success_body_bytes
126        } else {
127            self.error_body_bytes
128        }
129    }
130
131    /// Returns the larger status-class bound needed for caller response storage.
132    #[must_use]
133    pub const fn max_body_bytes(self) -> usize {
134        if self.success_body_bytes > self.error_body_bytes {
135            self.success_body_bytes
136        } else {
137            self.error_body_bytes
138        }
139    }
140
141    /// Returns the media policy selected by the final status class.
142    #[must_use]
143    pub const fn media_policy(self, status: StatusCode) -> ResponseMediaPolicy<'a> {
144        if status.is_success() {
145            self.success_media
146        } else {
147            self.error_media
148        }
149    }
150
151    /// Reports whether an operation admitted retention of this response header.
152    #[must_use]
153    pub fn admits_header(self, name: &str) -> bool {
154        self.admitted_headers
155            .iter()
156            .take(self.admitted_header_count)
157            .flatten()
158            .any(|candidate| candidate.eq_ignore_ascii_case(name))
159    }
160
161    /// Returns the informational-response limit.
162    #[must_use]
163    pub const fn informational_limit(self) -> u8 {
164        self.informational_limit
165    }
166
167    /// Returns the explicit trailer policy.
168    #[must_use]
169    pub const fn trailer_policy(self) -> TrailerPolicy {
170        self.trailer_policy
171    }
172}
173
174/// Informational or final-response selection failure.
175#[derive(Clone, Copy, Debug, Eq, PartialEq)]
176pub enum InformationalResponseError {
177    /// `101 Switching Protocols` is never admitted by buffered cloud requests.
178    SwitchingProtocols,
179    /// Too many informational responses preceded the final response.
180    TooManyInformationalResponses,
181    /// A final response was still informational.
182    MissingFinalResponse,
183}
184
185impl_static_error!(InformationalResponseError,
186    Self::SwitchingProtocols => "switching protocols is forbidden",
187    Self::TooManyInformationalResponses => "too many informational responses",
188    Self::MissingFinalResponse => "final HTTP response is missing",
189);
190
191/// Small state machine for bounded informational-response handling.
192#[derive(Clone, Copy, Debug, Eq, PartialEq)]
193pub struct InformationalResponseTracker {
194    limit: u8,
195    observed: u8,
196}
197
198impl InformationalResponseTracker {
199    /// Creates a tracker from a validated response policy.
200    #[must_use]
201    pub const fn new(policy: RawResponsePolicy<'_>) -> Self {
202        Self {
203            limit: policy.informational_limit,
204            observed: 0,
205        }
206    }
207
208    /// Observes one response head and reports whether it is the final head.
209    pub fn observe(&mut self, status: StatusCode) -> Result<bool, InformationalResponseError> {
210        if status.get() == 101 {
211            return Err(InformationalResponseError::SwitchingProtocols);
212        }
213        if status.get() < 200 {
214            self.observed = self
215                .observed
216                .checked_add(1)
217                .ok_or(InformationalResponseError::TooManyInformationalResponses)?;
218            if self.observed > self.limit {
219                return Err(InformationalResponseError::TooManyInformationalResponses);
220            }
221            return Ok(false);
222        }
223        Ok(true)
224    }
225
226    /// Returns the number of admitted informational heads.
227    #[must_use]
228    pub const fn observed(self) -> u8 {
229        self.observed
230    }
231}
232
233/// Blocking execution of one already validated raw HTTP request.
234pub trait BlockingRawHttpExecutor {
235    /// Executor-specific phased failure.
236    type Error;
237
238    /// Executes exactly once without implicit authentication or retries.
239    ///
240    /// Implementations must use [`ResponseWriter::begin_attempt`]. Response
241    /// mutation and commitment are available only through the returned guard.
242    fn execute(
243        &self,
244        request: TransportRequest<'_>,
245        policy: RawResponsePolicy<'_>,
246        response: &mut ResponseWriter<'_>,
247    ) -> Result<(), Self::Error>;
248}
249
250/// Runtime-neutral asynchronous raw HTTP execution.
251pub trait AsyncRawHttpExecutor {
252    /// Executor-specific phased failure.
253    type Error;
254
255    /// Executes exactly once without implicit authentication or retries.
256    ///
257    /// Implementations must use [`ResponseWriter::begin_attempt`]. Response
258    /// mutation and commitment are available only through the returned guard,
259    /// which clears state across future cancellation.
260    fn execute<'executor, 'request, 'policy, 'writer>(
261        &'executor self,
262        request: TransportRequest<'request>,
263        policy: RawResponsePolicy<'policy>,
264        response: &'writer mut ResponseWriter<'_>,
265    ) -> impl Future<Output = Result<(), Self::Error>> + Send + 'writer
266    where
267        'executor: 'writer,
268        'request: 'writer,
269        'policy: 'writer;
270}
271
272fn validate_media(policy: ResponseMediaPolicy<'_>) -> Result<(), RawResponsePolicyError> {
273    let media = match policy {
274        ResponseMediaPolicy::Required(media) | ResponseMediaPolicy::Optional(media) => media,
275        ResponseMediaPolicy::Forbidden => return Ok(()),
276    };
277    if media.is_empty() {
278        return Err(RawResponsePolicyError::MissingMediaType);
279    }
280    for (index, value) in media.iter().enumerate() {
281        if media.get(..index).is_some_and(|seen| {
282            seen.iter()
283                .any(|candidate| candidate.as_str().eq_ignore_ascii_case(value.as_str()))
284        }) {
285            return Err(RawResponsePolicyError::DuplicateMediaType);
286        }
287    }
288    Ok(())
289}
290
291fn validate_headers(headers: &[HeaderName<'_>]) -> Result<(), RawResponsePolicyError> {
292    if headers.len() > super::MAX_RESPONSE_HEADERS {
293        return Err(RawResponsePolicyError::TooManyAdmittedHeaders);
294    }
295    for (index, header) in headers.iter().enumerate() {
296        if is_unsafe_response_header(header.as_str()) {
297            return Err(RawResponsePolicyError::UnsafeAdmittedHeader);
298        }
299        if headers.get(..index).is_some_and(|seen| {
300            seen.iter()
301                .any(|candidate| candidate.eq_ignore_ascii_case(header.as_str()))
302        }) {
303            return Err(RawResponsePolicyError::DuplicateAdmittedHeader);
304        }
305    }
306    Ok(())
307}
308
309fn is_unsafe_response_header(name: &str) -> bool {
310    [
311        "authorization",
312        "connection",
313        "cookie",
314        "proxy-authenticate",
315        "proxy-authorization",
316        "set-cookie",
317        "te",
318        "trailer",
319        "transfer-encoding",
320        "upgrade",
321    ]
322    .iter()
323    .any(|candidate| name.eq_ignore_ascii_case(candidate))
324}
325
326#[cfg(test)]
327mod tests {
328    use super::{
329        InformationalResponseError, InformationalResponseTracker, RawResponsePolicy,
330        RawResponsePolicyError, ResponseMediaPolicy,
331    };
332    use crate::transport::{HeaderName, MediaType, StatusCode};
333
334    fn policy(limit: u8) -> Result<RawResponsePolicy<'static>, RawResponsePolicyError> {
335        RawResponsePolicy::new(
336            1024,
337            256,
338            ResponseMediaPolicy::Required(&[MediaType::JSON]),
339            ResponseMediaPolicy::Optional(&[MediaType::JSON]),
340            &[],
341            limit,
342        )
343    }
344
345    #[test]
346    fn selects_independent_success_and_error_limits() {
347        let Ok(policy) = policy(2) else {
348            return;
349        };
350        assert_eq!(policy.body_limit(StatusCode::OK), 1024);
351        assert_eq!(
352            policy.body_limit(StatusCode::new(400).unwrap_or(StatusCode::TOO_MANY_REQUESTS)),
353            256
354        );
355    }
356
357    #[test]
358    fn bounds_informationals_and_rejects_switching_protocols() {
359        let Ok(policy) = policy(2) else {
360            return;
361        };
362        let mut tracker = InformationalResponseTracker::new(policy);
363        let early = StatusCode::new(103).unwrap_or(StatusCode::OK);
364        assert_eq!(tracker.observe(early), Ok(false));
365        assert_eq!(tracker.observe(early), Ok(false));
366        assert_eq!(
367            tracker.observe(early),
368            Err(InformationalResponseError::TooManyInformationalResponses)
369        );
370        let switching = StatusCode::new(101).unwrap_or(StatusCode::OK);
371        assert_eq!(
372            InformationalResponseTracker::new(policy).observe(switching),
373            Err(InformationalResponseError::SwitchingProtocols)
374        );
375    }
376
377    #[test]
378    fn rejects_unsafe_and_duplicate_admitted_headers() {
379        let unsafe_header = HeaderName::new("set-cookie");
380        assert!(unsafe_header.is_ok());
381        if let Ok(unsafe_header) = unsafe_header {
382            assert!(matches!(
383                RawResponsePolicy::new(
384                    1,
385                    1,
386                    ResponseMediaPolicy::Optional(&[MediaType::JSON]),
387                    ResponseMediaPolicy::Optional(&[MediaType::JSON]),
388                    &[unsafe_header],
389                    0,
390                ),
391                Err(RawResponsePolicyError::UnsafeAdmittedHeader)
392            ));
393        }
394        let first = HeaderName::new("x-request-id");
395        let second = HeaderName::new("X-Request-ID");
396        assert!(first.is_ok() && second.is_ok());
397        if let (Ok(first), Ok(second)) = (first, second) {
398            assert!(matches!(
399                RawResponsePolicy::new(
400                    1,
401                    1,
402                    ResponseMediaPolicy::Optional(&[MediaType::JSON]),
403                    ResponseMediaPolicy::Optional(&[MediaType::JSON]),
404                    &[first, second],
405                    0,
406                ),
407                Err(RawResponsePolicyError::DuplicateAdmittedHeader)
408            ));
409        }
410    }
411}