Skip to main content

aws_smithy_http_server/schema/protocol/
rpc_v2_cbor.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use crate::protocol::rpc_v2_cbor::SMITHY_PROTOCOL_HEADER;
7use aws_smithy_cbor::codec::{CborCodec, CborDeserializer, CborSerializer};
8use aws_smithy_runtime_api::http::Headers;
9use aws_smithy_schema::codec::Codec;
10use aws_smithy_schema::serde::{SerdeError, SerializableStruct, ShapeDeserializer, ShapeSerializer};
11use aws_smithy_schema::{shape_id, Schema, ShapeId};
12
13use crate::body::BoxBody;
14use crate::protocol::rpc_v2_cbor::rejection::RequestRejection;
15use crate::protocol::rpc_v2_cbor::runtime_error::RuntimeError;
16use crate::response::{IntoResponse, Response};
17use crate::schema::{DeserializeError, HttpModeledError};
18
19use super::discriminator::TYPE_MEMBER;
20use super::response::{
21    log_serialize_failure, serialize_modeled_error_response, stamp_error_extension, stamp_validation_extension,
22    ResponseBindings,
23};
24use super::{BodyDirective, EventStreamFraming, MetadataRoutedProtocol, ServerProtocol};
25
26/// Stateful schema-driven Smithy RPC v2 CBOR protocol implementation.
27///
28/// Non-POST requests return `405` by default. Set the boolean
29/// `customizationConfig.protocols["smithy.protocols#rpcv2Cbor"].methodNotAllowedAsNotFound`
30/// to `true` to return the Java server's unknown-operation `404` instead.
31#[derive(Debug)]
32pub struct RpcV2Cbor {
33    pub(crate) inner: crate::schema::protocol::rpc::RpcProtocol<RpcV2CborSerde>,
34    method_not_allowed_as_not_found: bool,
35    legacy_routing_errors: bool,
36}
37
38impl Default for RpcV2Cbor {
39    fn default() -> Self {
40        Self {
41            inner: crate::schema::protocol::rpc::RpcProtocol::new(
42                RpcV2CborSerde::default(),
43                "application/cbor",
44                None,
45                crate::schema::protocol::rpc::RpcAccept::ModeledOutput,
46                crate::schema::protocol::rpc::RpcStreaming::EventStreamContentType,
47            ),
48            method_not_allowed_as_not_found: false,
49            legacy_routing_errors: false,
50        }
51    }
52}
53
54/// Supplies the server serializer to both HTTP responses and event-stream payloads.
55/// A structure-prefix hook writes error discriminators before modeled members, including
56/// nested errors. Deserialization and byte encoding remain the underlying codec's responsibility.
57#[derive(Debug)]
58pub(crate) struct RpcV2CborSerde(CborCodec);
59
60impl Default for RpcV2CborSerde {
61    fn default() -> Self {
62        Self(CborCodec::new(
63            aws_smithy_cbor::codec::CborCodecSettings::default()
64                .enforce_strictness(true)
65                .use_definite_lengths(true),
66        ))
67    }
68}
69
70impl Codec for RpcV2CborSerde {
71    type Serializer = CborSerializer;
72    type Deserializer<'a> = CborDeserializer<'a>;
73
74    fn create_serializer(&self) -> Self::Serializer {
75        self.0.create_serializer().with_struct_prefix(write_error_type)
76    }
77
78    fn create_deserializer<'a>(&self, input: &'a [u8]) -> Self::Deserializer<'a> {
79        self.0.create_deserializer(input)
80    }
81}
82
83fn write_error_type(schema: &Schema<'_>, serializer: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
84    if schema
85        .traits()
86        .is_some_and(|traits| traits.contains_fqn("smithy.api#error"))
87    {
88        serializer.write_string(&TYPE_MEMBER, schema.shape_id().as_str())?;
89    }
90    Ok(())
91}
92
93static PROTOCOL_ID: ShapeId<'static> = shape_id!("smithy.protocols", "rpcv2Cbor");
94const CONTENT_TYPE: &str = "application/cbor";
95const SMITHY_PROTOCOL_VALUE: http::HeaderValue = http::HeaderValue::from_static("rpc-v2-cbor");
96
97/// The `smithy-protocol` and `Accept` request headers are validated by the router; responses
98/// carry `smithy-protocol: rpc-v2-cbor`.
99fn with_protocol_header(mut response: Response) -> Response {
100    response
101        .headers_mut()
102        .insert(SMITHY_PROTOCOL_HEADER, SMITHY_PROTOCOL_VALUE);
103    response
104}
105
106impl MetadataRoutedProtocol for RpcV2Cbor {
107    fn from_build_context(
108        ctx: &crate::schema::ProtocolBuildContext<'_>,
109    ) -> Result<Self, crate::schema::routing::RouterBuildError> {
110        Ok(Self {
111            legacy_routing_errors: ctx.service.protocols().len() == 1,
112            method_not_allowed_as_not_found: crate::schema::settings::get::<bool>(
113                ctx.settings,
114                "methodNotAllowedAsNotFound",
115            )?
116            .unwrap_or(false),
117            ..Self::default()
118        })
119    }
120
121    fn build_router(
122        &self,
123        ctx: crate::schema::routing::RouterBuildContext<'_>,
124    ) -> Result<
125        impl crate::schema::routing::MetadataProtocolRouter + 'static + use<>,
126        crate::schema::routing::RouterBuildError,
127    > {
128        crate::schema::routing::rpc_v2_cbor_router(&ctx)
129    }
130
131    fn event_stream_framing(&self) -> Option<EventStreamFraming<'_>> {
132        Some(EventStreamFraming::new(self.inner.codec(), CONTENT_TYPE).initial_messages_in_frames(true))
133    }
134}
135
136impl ServerProtocol for RpcV2Cbor {
137    fn protocol_id(&self) -> &'static ShapeId<'static> {
138        &PROTOCOL_ID
139    }
140
141    fn validate_request_headers(
142        &self,
143        operation: &crate::schema::OperationSchema<'_>,
144        headers: &Headers,
145    ) -> Result<(), DeserializeError> {
146        self.inner.check_accept(operation.output(), headers)
147    }
148
149    fn request_body_requirement(&self, operation: &crate::schema::OperationSchema<'_>) -> BodyDirective {
150        if self.inner.reads_request_body(operation.input()) {
151            BodyDirective::Collect
152        } else {
153            BodyDirective::Skip
154        }
155    }
156
157    fn deserialize_request<'a>(
158        &'a self,
159        input: &Schema<'_>,
160        request: &'a aws_smithy_runtime_api::http::Request<bytes::Bytes>,
161    ) -> Result<Box<dyn ShapeDeserializer + 'a>, DeserializeError> {
162        self.inner.deserialize_request(input, request)
163    }
164
165    fn serialize_response(&self, output: &Schema<'_>, value: &dyn SerializableStruct) -> Response {
166        self.inner
167            .serialize_response(output, value)
168            .map(with_protocol_header)
169            .unwrap_or_else(serialization_failure)
170    }
171
172    fn serialize_streaming_response(
173        &self,
174        output: &Schema<'_>,
175        value: &dyn SerializableStruct,
176        streaming_member: &Schema<'_>,
177        body: Option<BoxBody>,
178    ) -> Response {
179        if streaming_member.shape_type() == aws_smithy_schema::ShapeType::Blob {
180            let Some(body) = body else {
181                return self.serialize_response(output, value);
182            };
183            return match super::rpc_v2_cbor_streaming::body(self.inner.codec(), output, value, streaming_member, body) {
184                Ok(body) => with_protocol_header(
185                    http::Response::builder()
186                        .status(200)
187                        .header(http::header::CONTENT_TYPE, CONTENT_TYPE)
188                        .body(body)
189                        .expect("static CBOR response headers are valid"),
190                ),
191                Err(err) => serialization_failure(err),
192            };
193        }
194        self.inner
195            .serialize_streaming_response(output, value, body.unwrap_or_else(crate::body::empty))
196            .map(with_protocol_header)
197            .unwrap_or_else(serialization_failure)
198    }
199
200    fn serialize_error(&self, error: &dyn HttpModeledError) -> Response {
201        let schema = error.schema();
202        serialize_modeled_error_response(
203            self.inner.codec(),
204            schema,
205            error,
206            error.status_code(),
207            ResponseBindings::BodyOnly,
208            CONTENT_TYPE,
209        )
210        .map(with_protocol_header)
211        .map(|response| stamp_error_extension(response, schema.shape_id().shape_name()))
212        .unwrap_or_else(serialization_failure)
213    }
214
215    /// Method mismatches return the generated serde server's bare `405` by default. The
216    /// `methodNotAllowedAsNotFound` setting selects the unknown-operation `404` instead.
217    /// Unsupported operations return an empty `501`. Every other routing rejection, including
218    /// framing violations such as a forbidden `x-amz-target`/`x-amzn-target` header or an invalid
219    /// `smithy-protocol` value, returns the generated serde empty `404` for a single-protocol service.
220    /// Multi-protocol services serialize the CBOR unknown-operation error after claiming.
221    fn serialize_routing_error(&self, err: &crate::schema::routing::RoutingError) -> Response {
222        match err.kind() {
223            crate::schema::routing::RoutingErrorKind::UnsupportedOperation => crate::operation::empty_not_implemented(),
224            crate::schema::routing::RoutingErrorKind::MethodNotAllowed if !self.method_not_allowed_as_not_found => {
225                crate::routing::method_disallowed()
226            }
227            crate::schema::routing::RoutingErrorKind::MethodNotAllowed => {
228                self.serialize_error(&crate::schema::routing::RoutingError::unknown_operation())
229            }
230            _ if self.legacy_routing_errors => {
231                IntoResponse::<RpcV2Cbor>::into_response(crate::protocol::rpc_v2_cbor::router::Error::NotFound)
232            }
233            _ => self.serialize_error(&crate::schema::routing::RoutingError::unknown_operation()),
234        }
235    }
236
237    fn serialize_runtime_error(&self, error: crate::runtime_error::RuntimeError) -> Response {
238        IntoResponse::<RpcV2Cbor>::into_response(RuntimeError::from(error))
239    }
240
241    /// rpcv2Cbor's `From<RequestRejection>` collapses every transport failure into a 400
242    /// `Serialization` (body `0xa0`, no `__type`, upstream #3716, and no `smithy-protocol`
243    /// header). A constraint violation serializes the modeled validation error, `__type` first,
244    /// full shape ID, but through the response engine directly rather than
245    /// [`Self::serialize_error`]: rejection responses must NOT carry the `smithy-protocol`
246    /// header, which is reserved for handler-returned responses. An invalid initial request
247    /// frame uses the shared validation error's CBOR encoding.
248    fn serialize_rejection(&self, err: DeserializeError) -> Response {
249        match err {
250            DeserializeError::InitialRequestFrame(reason) => {
251                self.serialize_runtime_error(crate::runtime_error::RuntimeError::Validation(reason))
252            }
253            DeserializeError::Serde(err) => {
254                IntoResponse::<RpcV2Cbor>::into_response(RuntimeError::Serialization(crate::Error::new(err)))
255            }
256            DeserializeError::BodyCollection(err) => {
257                IntoResponse::<RpcV2Cbor>::into_response(RuntimeError::Serialization(crate::Error::new(err)))
258            }
259            DeserializeError::UnsupportedMediaType(reason) => IntoResponse::<RpcV2Cbor>::into_response(
260                RuntimeError::from(RequestRejection::MissingContentType(*reason)),
261            ),
262            DeserializeError::NotAcceptable => {
263                IntoResponse::<RpcV2Cbor>::into_response(RuntimeError::from(RequestRejection::NotAcceptable))
264            }
265            DeserializeError::ConstraintViolation(err) => {
266                let schema = err.schema();
267                serialize_modeled_error_response(
268                    self.inner.codec(),
269                    schema,
270                    &*err,
271                    err.status_code(),
272                    ResponseBindings::BodyOnly,
273                    CONTENT_TYPE,
274                )
275                .map(|response| stamp_error_extension(response, schema.shape_id().shape_name()))
276                .map(stamp_validation_extension)
277                .unwrap_or_else(serialization_failure)
278            }
279            DeserializeError::InternalFailure(err) => {
280                IntoResponse::<RpcV2Cbor>::into_response(RuntimeError::InternalFailure(err))
281            }
282        }
283    }
284}
285
286fn serialization_failure(err: SerdeError) -> Response {
287    log_serialize_failure(&err);
288    IntoResponse::<RpcV2Cbor>::into_response(RuntimeError::Serialization(crate::Error::new(err)))
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use aws_smithy_schema::codec::FinishSerializer;
295    use aws_smithy_schema::serde::SerializableStruct;
296    use aws_smithy_schema::{shape_id, ShapeType};
297
298    #[test]
299    fn ordinary_structure_has_no_discriminator() {
300        static NAME: Schema = Schema::new_member(shape_id!("test", "Output", "name"), ShapeType::String, "name", 0);
301        static OUTPUT: Schema = Schema::new_struct(shape_id!("test", "Output"), ShapeType::Structure, &[&NAME]);
302        struct Output;
303        impl SerializableStruct for Output {
304            fn schema(&self) -> &Schema<'_> {
305                &OUTPUT
306            }
307
308            fn serialize_members(&self, serializer: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
309                serializer.write_string(&NAME, "hello")
310            }
311        }
312        let mut serializer = RpcV2CborSerde::default().create_serializer();
313        serializer.write_struct(&OUTPUT, &Output).unwrap();
314        let mut expected = aws_smithy_cbor::Encoder::new(Vec::new());
315        expected.begin_map().str("name").str("hello").end();
316        assert_eq!(serializer.finish(), expected.into_writer());
317    }
318
319    #[test]
320    fn only_the_server_adds_error_types() {
321        static TRAITS: std::sync::LazyLock<aws_smithy_schema::TraitMap> = std::sync::LazyLock::new(|| {
322            let mut traits = aws_smithy_schema::TraitMap::new();
323            traits.insert(Box::new(aws_smithy_schema::StringTrait::new(
324                shape_id!("smithy.api", "error"),
325                "client",
326            )));
327            traits
328        });
329        static MESSAGE: Schema<'static> =
330            Schema::new_member(shape_id!("test", "Failure", "message"), ShapeType::String, "message", 0);
331        static ERROR: Schema<'static> =
332            Schema::new_struct(shape_id!("test", "Failure"), ShapeType::Structure, &[&MESSAGE]).with_traits(&TRAITS);
333        static MEMBER: Schema<'static> =
334            Schema::new_member(shape_id!("test", "Output", "error"), ShapeType::Structure, "error", 0);
335        static OUTPUT: Schema<'static> =
336            Schema::new_struct(shape_id!("test", "Output"), ShapeType::Structure, &[&MEMBER]);
337        struct Error;
338        impl SerializableStruct for Error {
339            fn schema(&self) -> &Schema<'_> {
340                &ERROR
341            }
342
343            fn serialize_members(&self, ser: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
344                ser.write_string(&MESSAGE, "failed")
345            }
346        }
347        struct Output;
348        impl SerializableStruct for Output {
349            fn schema(&self) -> &Schema<'_> {
350                &OUTPUT
351            }
352            fn serialize_members(&self, ser: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
353                ser.write_struct(&MEMBER, &Error)
354            }
355        }
356        for enabled in [false, true] {
357            let plain = CborCodec::default();
358            let server = RpcV2CborSerde::default();
359            let codec: &dyn aws_smithy_schema::codec::DynCodec = if enabled { &server } else { &plain };
360            for nesting in 0..6 {
361                let mut serializer = codec.create_serializer();
362                let list = Schema::new(shape_id!("test", "Errors"), ShapeType::List);
363                let map = Schema::new(shape_id!("test", "ErrorsByName"), ShapeType::Map);
364                let union_members = [&MEMBER];
365                let union = Schema::new_struct(shape_id!("test", "ErrorUnion"), ShapeType::Union, &union_members);
366                let write_error = |ser: &mut dyn ShapeSerializer| ser.write_struct(&MEMBER, &Error);
367                let write_entry = |ser: &mut dyn ShapeSerializer| {
368                    ser.write_string(&aws_smithy_schema::prelude::STRING, "error")?;
369                    write_error(ser)
370                };
371                match nesting {
372                    0 => serializer.write_struct(&ERROR, &Error),
373                    1 => serializer.write_struct(&OUTPUT, &Output),
374                    2 => serializer.write_list(&list, &write_error),
375                    3 => serializer.write_map(&map, &write_entry),
376                    4 => serializer.write_struct(&union, &Output),
377                    _ => serializer.write_map(&map, &|ser| {
378                        ser.write_string(&aws_smithy_schema::prelude::STRING, "error")?;
379                        ser.write_list(&list, &write_error)
380                    }),
381                }
382                .unwrap();
383                let actual = serializer.finish_boxed();
384                // Exact bytes also prove ordering and absence of duplicate discriminator keys.
385                let mut expected = aws_smithy_cbor::Encoder::new(Vec::new());
386                match nesting {
387                    1 | 3 | 4 => {
388                        expected.begin_map().str("error");
389                    }
390                    2 => {
391                        expected.begin_array();
392                    }
393                    5 => {
394                        expected.begin_map().str("error").begin_array();
395                    }
396                    _ => {}
397                }
398                expected.begin_map();
399                if enabled {
400                    expected.str("__type").str("test#Failure");
401                }
402                expected.str("message").str("failed").end();
403                if nesting != 0 {
404                    expected.end();
405                }
406                if nesting == 5 {
407                    expected.end();
408                }
409                assert_eq!(actual, expected.into_writer(), "server={enabled}, nesting={nesting}");
410            }
411        }
412    }
413}