Skip to main content

cratestack_axum/envelope_layer/
builder_payload.rs

1//! [`EnvelopeLayerBuilder::payload_media_types`] (cratestack#1168).
2
3use cratestack_core::{CratestackError, DEFAULT_PAYLOAD_MEDIA_TYPE, is_sealable_payload_type};
4
5use super::builder::EnvelopeLayerBuilder;
6
7impl EnvelopeLayerBuilder {
8    /// The payload media types the layer allows inside the seal: `request`
9    /// for what a client seals, `response` for what the layer seals back.
10    /// **Default: `application/cbor` for both**, so a layer that never calls
11    /// this changes nothing for anyone: a request that names no type is
12    /// bound and opened with the bytes 0.15.3 used (AAD and body), and
13    /// answered by a response whose AAD and body are 0.15.3's too. The one
14    /// difference a 0.15.3 client can see is a new response header,
15    /// `Cratestack-Payload-Type`, which it ignores.
16    ///
17    /// A client names the type of its request in the unbound
18    /// `Cratestack-Payload-Type` header and the response types it reads, in
19    /// order, in `Cratestack-Payload-Accept` (the same selector pattern as
20    /// `Cratestack-Contract`: they select among what is allowed and never
21    /// widen it). Both are authenticated by the binding, not by the headers:
22    /// a request binding names the request payload's type, a response
23    /// binding names the **response payload's own** type, and a response
24    /// repeats it in `Cratestack-Payload-Type`. A header that lies about the
25    /// request fails the signature with the coarse `401`. Binding version 2
26    /// and the COSE wire are unchanged.
27    ///
28    /// What an op allows is this set intersected with the route's declared
29    /// types ([`ResolvedRoute::with_payload_types`], filled from the schema's
30    /// capabilities by [`RestBindingResolver`] and [`RpcBindingResolver`]).
31    /// `/rpc/batch` stays CBOR both ways. An empty route list (the generated
32    /// reads and deletes, which take no payload) constrains nothing, and a
33    /// `GET`, `HEAD` or `DELETE` is not checked for the type its **empty**
34    /// payload names (bound as sent, never refused): a payload that is not
35    /// empty is held to this set and the route's once opened, and refused as
36    /// a sealed `415`.
37    ///
38    /// A retry under the same `Idempotency-Key` must repeat its
39    /// `Cratestack-Payload-Accept`: the idempotency layer fingerprints the
40    /// request's content type and body, not the types it reads, so a
41    /// response stored for the first attempt in a type the retry did not
42    /// negotiate is never sealed for it (a sealed `500`, fail closed).
43    ///
44    /// Refusals are unsigned and made before any key is looked up or nonce
45    /// spent: a selector header sent twice or malformed is a `400`, a request
46    /// type outside the op's set a `415` (`payload_type_unsupported`), and a
47    /// `Cratestack-Payload-Accept` with no type the op can answer in a `406`
48    /// (`payload_type_not_acceptable`). A handler's success in a type the
49    /// request did not negotiate is a sealed `500`; its error in one is
50    /// re-encoded in the transport's error shape, in the client's first
51    /// choice of CBOR or JSON, and sealed.
52    ///
53    /// [`build`](Self::build) refuses a type outside the grammar of
54    /// `cratestack_core::parse_payload_type`, one that may never be sealed
55    /// (`application/cose*`, `application/cbor-seq`, `text/event-stream`,
56    /// `multipart/*`), an empty request set, and a response set without
57    /// `application/cbor` or `application/json`, which the layer's own
58    /// errors are sealed in.
59    ///
60    /// ```
61    /// use cratestack_axum::envelope_layer::EnvelopeLayer;
62    /// # fn demo(envelope: impl cratestack_axum::envelope_layer::ServerEnvelope) {
63    /// // vpay's shape: forms in, JSON out, and CBOR for the generated clients.
64    /// let builder = EnvelopeLayer::builder(envelope, "payments", &[]).payload_media_types(
65    ///     ["application/cbor", "application/x-www-form-urlencoded"],
66    ///     ["application/cbor", "application/json"],
67    /// );
68    /// # drop(builder);
69    /// # }
70    /// ```
71    ///
72    /// [`ResolvedRoute::with_payload_types`]: super::ResolvedRoute::with_payload_types
73    /// [`RestBindingResolver`]: super::RestBindingResolver
74    /// [`RpcBindingResolver`]: super::RpcBindingResolver
75    #[must_use]
76    pub fn payload_media_types<I, J>(mut self, request: I, response: J) -> Self
77    where
78        I: IntoIterator,
79        I::Item: Into<String>,
80        J: IntoIterator,
81        J::Item: Into<String>,
82    {
83        self.payload_request = request.into_iter().map(Into::into).collect();
84        self.payload_response = response.into_iter().map(Into::into).collect();
85        self
86    }
87}
88
89/// The checks `build()` makes of the configured payload types.
90pub(super) fn validate(request: &[String], response: &[String]) -> Result<(), CratestackError> {
91    let invalid = |message: String| Err(CratestackError::Validation(message));
92    if request.is_empty() {
93        return invalid("payload_media_types(..): the request set must not be empty".to_owned());
94    }
95    for (side, types) in [("request", request), ("response", response)] {
96        if let Some(bad) = types.iter().find(|t| !is_sealable_payload_type(t)) {
97            return invalid(format!(
98                "payload_media_types(..): {bad:?} cannot be a sealed {side} payload type \
99                 (a lowercase `type/subtype` without parameters or wildcards, and never an \
100                 envelope, a stream or a multipart body)"
101            ));
102        }
103    }
104    // The layer's own errors, and a handler's foreign-typed ones, are sealed
105    // in a type the transport's error codec writes.
106    if !response
107        .iter()
108        .any(|t| t == DEFAULT_PAYLOAD_MEDIA_TYPE || t == "application/json")
109    {
110        return invalid(
111            "payload_media_types(..): the response set needs application/cbor or \
112             application/json, which the layer's own errors are sealed in"
113                .to_owned(),
114        );
115    }
116    Ok(())
117}