1use crate::codec::{CborCodec, CborCodecSettings};
9use crate::Decoder;
10use aws_smithy_runtime_api::client::orchestrator::Metadata;
11use aws_smithy_runtime_api::http::{Headers, Request, Response};
12use aws_smithy_schema::error_envelope::{parse_query_compatible_header, sanitize_error_code};
13use aws_smithy_schema::http_protocol::HttpRpcProtocol;
14use aws_smithy_schema::protocol::{ClientProtocolInner, ServiceShapeName};
15use aws_smithy_schema::serde::{SerdeError, SerializableStruct, ShapeDeserializer};
16use aws_smithy_schema::{shape_id, Schema, ShapeId};
17use aws_smithy_types::config_bag::ConfigBag;
18use aws_smithy_types::error::metadata::{Builder as ErrorMetadataBuilder, ErrorMetadata};
19
20#[derive(Debug)]
22pub struct RpcV2CborProtocol {
23 inner: HttpRpcProtocol<CborCodec>,
24}
25
26impl RpcV2CborProtocol {
27 pub fn new() -> Self {
29 Self {
30 inner: HttpRpcProtocol::new(
31 shape_id!("smithy.protocols", "rpcv2Cbor"),
32 CborCodec::new(CborCodecSettings::default()),
33 "application/cbor",
34 ),
35 }
36 }
37}
38
39impl Default for RpcV2CborProtocol {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45impl ClientProtocolInner for RpcV2CborProtocol {
46 type Request = Request;
47 type Response = Response;
48
49 fn protocol_id(&self) -> &ShapeId<'static> {
50 self.inner.protocol_id()
51 }
52
53 fn serialize_request(
54 &self,
55 input: &dyn SerializableStruct,
56 input_schema: &Schema<'_>,
57 endpoint: &str,
58 cfg: &ConfigBag,
59 ) -> Result<Request, SerdeError> {
60 let route = rpc_v2_cbor_route(cfg);
72 let endpoint = route.as_deref().unwrap_or(endpoint);
73 let mut request = self
74 .inner
75 .serialize_request(input, input_schema, endpoint, cfg)?;
76
77 request
89 .headers_mut()
90 .insert("smithy-protocol", "rpc-v2-cbor");
91 request.headers_mut().insert("accept", "application/cbor");
92
93 Ok(request)
94 }
95
96 fn deserialize_response<'a>(
97 &self,
98 response: &'a Response,
99 output_schema: &Schema<'_>,
100 cfg: &'a ConfigBag,
101 ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
102 self.inner
103 .deserialize_response(response, output_schema, cfg)
104 }
105
106 fn payload_codec(&self) -> Option<&dyn aws_smithy_schema::codec::DynCodec> {
107 self.inner.payload_codec()
108 }
109
110 fn event_stream_media_type(&self) -> Option<&str> {
116 Some("application/cbor")
117 }
118
119 fn parse_event_stream_error_metadata(
124 &self,
125 payload: &[u8],
126 ) -> Result<ErrorMetadataBuilder, SerdeError> {
127 parse_error_envelope_metadata(payload, &Headers::new())
128 }
129
130 fn update_endpoint(
131 &self,
132 request: &mut Request,
133 endpoint: &aws_smithy_types::endpoint::Endpoint,
134 cfg: &ConfigBag,
135 ) -> Result<(), SerdeError> {
136 self.inner.update_endpoint(request, endpoint, cfg)
137 }
138
139 fn parse_error_metadata(
140 &self,
141 response: &Response,
142 _cfg: &ConfigBag,
143 ) -> Result<ErrorMetadataBuilder, SerdeError> {
144 let body = response.body().bytes().unwrap_or(&[]);
145 parse_error_envelope_metadata(body, response.headers())
146 }
147}
148
149fn rpc_v2_cbor_route(cfg: &ConfigBag) -> Option<String> {
160 let service = cfg.load::<ServiceShapeName>()?;
161 let operation = cfg.load::<Metadata>()?;
162 Some(format!(
163 "/service/{}/operation/{}",
164 service.as_str(),
165 operation.name()
166 ))
167}
168
169pub(crate) fn parse_error_envelope_metadata(
183 response_body: &[u8],
184 response_headers: &Headers,
185) -> Result<ErrorMetadataBuilder, SerdeError> {
186 let mut builder = if response_body.is_empty() {
187 ErrorMetadata::builder()
188 } else {
189 parse_error_body(response_body)?
190 };
191
192 if let Some((qc_code, qc_type)) = parse_query_compatible_header(response_headers) {
195 builder = builder.code(qc_code).custom("type", qc_type);
196 }
197
198 Ok(builder)
199}
200
201fn parse_error_body(response_body: &[u8]) -> Result<ErrorMetadataBuilder, SerdeError> {
202 let decoder = &mut Decoder::new(response_body);
203 let mut builder = ErrorMetadata::builder();
204
205 match decoder.map().map_err(deser_err)? {
206 None => loop {
208 match decoder.datatype().map_err(deser_err)? {
209 crate::data::Type::Break => {
210 decoder.skip().map_err(deser_err)?;
211 break;
212 }
213 _ => {
214 builder = error_code_and_message(builder, decoder)?;
215 }
216 }
217 },
218 Some(n) => {
220 for _ in 0..n {
221 builder = error_code_and_message(builder, decoder)?;
222 }
223 }
224 }
225
226 Ok(builder)
227}
228
229fn error_code_and_message(
230 mut builder: ErrorMetadataBuilder,
231 decoder: &mut Decoder,
232) -> Result<ErrorMetadataBuilder, SerdeError> {
233 let key = decoder.str().map_err(deser_err)?;
234 builder = match key.as_ref() {
235 "__type" => {
236 match decoder.str() {
241 Ok(code) => builder.code(sanitize_error_code(&code)),
242 Err(_) => builder,
243 }
244 }
245 "message" | "Message" | "errorMessage" => {
246 match decoder.str() {
249 Ok(message) => builder.message(message),
250 Err(_) => builder,
251 }
252 }
253 _ => {
254 decoder.skip().map_err(deser_err)?;
255 builder
256 }
257 };
258 Ok(builder)
259}
260
261fn deser_err(e: crate::decode::DeserializeError) -> SerdeError {
262 SerdeError::invalid_input(e.to_string())
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::Encoder;
269 use aws_smithy_runtime_api::http::{Response, StatusCode};
270 use aws_smithy_types::body::SdkBody;
271 use aws_smithy_types::config_bag::ConfigBag;
272
273 fn cbor_response(body: Vec<u8>) -> Response {
274 Response::new(StatusCode::try_from(400).unwrap(), SdkBody::from(body))
275 }
276
277 fn encode_envelope(type_value: Option<&str>, message_value: Option<(&str, &str)>) -> Vec<u8> {
278 let mut entries: Vec<(&str, &str)> = vec![];
279 if let Some(t) = type_value {
280 entries.push(("__type", t));
281 }
282 if let Some((k, v)) = message_value {
283 entries.push((k, v));
284 }
285 let mut encoder = Encoder::new(Vec::new());
286 encoder.map(entries.len());
287 for (k, v) in &entries {
288 encoder.str(k).str(v);
289 }
290 encoder.into_writer()
291 }
292
293 #[test]
294 fn parse_error_metadata_extracts_code_and_message() {
295 let body = encode_envelope(Some("InvalidGreeting"), Some(("message", "Hi")));
296 let response = cbor_response(body);
297 let cfg = ConfigBag::base();
298 let protocol = RpcV2CborProtocol::new();
299
300 let meta = protocol
301 .parse_error_metadata(&response, &cfg)
302 .expect("parse succeeds")
303 .build();
304
305 assert_eq!(meta.code(), Some("InvalidGreeting"));
306 assert_eq!(meta.message(), Some("Hi"));
307 }
308
309 #[test]
310 fn parse_error_metadata_sanitizes_namespaced_code() {
311 let body = encode_envelope(
312 Some("aws.protocoltests.rpcv2cbor#InvalidGreeting:http://example/"),
313 None,
314 );
315 let response = cbor_response(body);
316 let cfg = ConfigBag::base();
317 let protocol = RpcV2CborProtocol::new();
318
319 let meta = protocol
320 .parse_error_metadata(&response, &cfg)
321 .expect("parse succeeds")
322 .build();
323
324 assert_eq!(meta.code(), Some("InvalidGreeting"));
325 }
326
327 #[test]
328 fn parse_error_metadata_accepts_alternate_message_keys() {
329 for key in &["Message", "errorMessage"] {
330 let body = encode_envelope(Some("X"), Some((key, "msg")));
331 let response = cbor_response(body);
332 let cfg = ConfigBag::base();
333 let protocol = RpcV2CborProtocol::new();
334
335 let meta = protocol
336 .parse_error_metadata(&response, &cfg)
337 .expect("parse succeeds")
338 .build();
339
340 assert_eq!(meta.message(), Some("msg"), "key={}", key);
341 }
342 }
343
344 #[test]
345 fn parse_error_metadata_empty_body_returns_empty_builder() {
346 let response = cbor_response(Vec::new());
347 let cfg = ConfigBag::base();
348 let protocol = RpcV2CborProtocol::new();
349
350 let meta = protocol
351 .parse_error_metadata(&response, &cfg)
352 .expect("parse succeeds")
353 .build();
354
355 assert_eq!(meta.code(), None);
356 assert_eq!(meta.message(), None);
357 }
358
359 #[test]
360 fn parse_error_metadata_malformed_body_returns_error() {
361 let response = cbor_response(vec![0xff]);
363 let cfg = ConfigBag::base();
364 let protocol = RpcV2CborProtocol::new();
365
366 let err = protocol
367 .parse_error_metadata(&response, &cfg)
368 .expect_err("malformed body should fail");
369 assert!(
370 matches!(err, SerdeError::InvalidInput { .. }),
371 "expected InvalidInput, got {:?}",
372 err
373 );
374 }
375
376 #[test]
377 fn parse_error_metadata_ignores_unknown_keys() {
378 let mut encoder = Encoder::new(Vec::new());
381 encoder.map(3);
382 encoder.str("__type").str("InvalidGreeting");
383 encoder.str("not_a_known_key").str("ignore me");
384 encoder.str("message").str("Hi");
385 let body = encoder.into_writer();
386 let response = cbor_response(body);
387 let cfg = ConfigBag::base();
388 let protocol = RpcV2CborProtocol::new();
389
390 let meta = protocol
391 .parse_error_metadata(&response, &cfg)
392 .expect("parse succeeds")
393 .build();
394
395 assert_eq!(meta.code(), Some("InvalidGreeting"));
396 assert_eq!(meta.message(), Some("Hi"));
397 }
398
399 #[test]
400 fn parse_error_metadata_query_compat_header_overrides_body_code() {
401 let body = encode_envelope(Some("CustomCodeError"), Some(("message", "Hi")));
406 let mut response = cbor_response(body);
407 response.headers_mut().insert(
408 "x-amzn-query-error".to_string(),
409 "Customized;Sender".to_string(),
410 );
411 let cfg = ConfigBag::base();
412 let protocol = RpcV2CborProtocol::new();
413
414 let meta = protocol
415 .parse_error_metadata(&response, &cfg)
416 .expect("parse succeeds")
417 .build();
418
419 assert_eq!(meta.code(), Some("Customized"));
420 assert_eq!(meta.message(), Some("Hi"));
421 assert_eq!(meta.extra("type"), Some("Sender"));
422 }
423
424 struct EmptyInput;
426 impl aws_smithy_schema::serde::SerializableStruct for EmptyInput {
427 fn schema(&self) -> &Schema<'_> {
428 &INPUT_SCHEMA
429 }
430
431 fn serialize_members(
432 &self,
433 _: &mut dyn aws_smithy_schema::serde::ShapeSerializer,
434 ) -> Result<(), SerdeError> {
435 Ok(())
436 }
437 }
438
439 static INPUT_SCHEMA: Schema<'static> = Schema::new(
440 aws_smithy_schema::shape_id!("smithy.example", "GetStatsInput"),
441 aws_smithy_schema::ShapeType::Structure,
442 );
443
444 fn cfg_with_names(service: &'static str, operation: &'static str) -> ConfigBag {
446 let mut layer = aws_smithy_types::config_bag::Layer::new("test");
447 layer.store_put(ServiceShapeName::new(service));
448 layer.store_put(Metadata::new(operation, "Some Sdk Id"));
449 ConfigBag::of_layers(vec![layer])
450 }
451
452 fn serialize_with(cfg: &ConfigBag, endpoint: &str) -> Request {
453 RpcV2CborProtocol::new()
454 .serialize_request(&EmptyInput, &INPUT_SCHEMA, endpoint, cfg)
455 .expect("serialization succeeds")
456 }
457
458 #[test]
459 fn serialize_request_uses_canonical_rpc_route() {
460 let cfg = cfg_with_names("PokemonService", "GetServerStatistics");
461 let request = serialize_with(&cfg, "");
462 assert_eq!(
463 "/service/PokemonService/operation/GetServerStatistics",
464 request.uri()
465 );
466 assert_eq!("POST", request.method());
467 }
468
469 #[test]
475 fn serialize_request_sets_protocol_framing_headers() {
476 let cfg = cfg_with_names("PokemonService", "GetServerStatistics");
477 let request = serialize_with(&cfg, "");
478 assert_eq!(
479 Some("rpc-v2-cbor"),
480 request.headers().get("smithy-protocol")
481 );
482 assert_eq!(Some("application/cbor"), request.headers().get("accept"));
483 }
484
485 #[test]
489 fn serialize_request_sets_framing_headers_without_model_names() {
490 let cfg = ConfigBag::base();
491 let request = serialize_with(&cfg, "/some/path");
492 assert_eq!("/some/path", request.uri());
493 assert_eq!(
494 Some("rpc-v2-cbor"),
495 request.headers().get("smithy-protocol")
496 );
497 assert_eq!(Some("application/cbor"), request.headers().get("accept"));
498 }
499
500 #[test]
506 fn serialize_request_overrides_endpoint_from_another_protocol() {
507 let cfg = cfg_with_names("PokemonService", "GetServerStatistics");
508 for endpoint in ["/", "/stats", "/service/Wrong/operation/Wrong"] {
509 let request = serialize_with(&cfg, endpoint);
510 assert_eq!(
511 "/service/PokemonService/operation/GetServerStatistics",
512 request.uri(),
513 "endpoint {endpoint:?} must not override the protocol's route",
514 );
515 }
516 }
517
518 #[test]
522 fn serialize_request_falls_back_to_supplied_endpoint() {
523 let cfg = ConfigBag::base();
524 assert_eq!(
525 "/service/Fallback/operation/Op",
526 serialize_with(&cfg, "/service/Fallback/operation/Op").uri()
527 );
528 assert_eq!("/", serialize_with(&cfg, "").uri());
530 }
531
532 #[test]
535 fn serialize_request_falls_back_when_only_metadata_is_present() {
536 let mut layer = aws_smithy_types::config_bag::Layer::new("test");
537 layer.store_put(Metadata::new("GetServerStatistics", "Pokemon Service"));
538 let cfg = ConfigBag::of_layers(vec![layer]);
539 assert_eq!("/", serialize_with(&cfg, "").uri());
540 }
541
542 #[test]
545 fn service_shape_name_accepts_runtime_strings() {
546 let owned: String = ["Pokemon", "Service"].concat();
547 let mut layer = aws_smithy_types::config_bag::Layer::new("test");
548 layer.store_put(ServiceShapeName::new(owned));
549 layer.store_put(Metadata::new("GetServerStatistics", "Pokemon Service"));
550 let cfg = ConfigBag::of_layers(vec![layer]);
551 assert_eq!(
552 "/service/PokemonService/operation/GetServerStatistics",
553 serialize_with(&cfg, "").uri()
554 );
555 }
556}