1use 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#[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#[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
97fn 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 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 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 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}