1use std::sync::Arc;
9
10use crate::codec::{XmlCodec, XmlCodecSettings, XmlDeserializer};
11use crate::decode::{try_data, Document};
12use aws_smithy_schema::http_protocol::{http_error_deserializer, HttpBindingProtocol};
13use aws_smithy_schema::serde::SerdeError;
14use aws_smithy_schema::Schema;
15use aws_smithy_schema::{shape_id, ShapeId};
16use aws_smithy_types::config_bag::ConfigBag;
17use aws_smithy_types::date_time::Format as TimestampFormat;
18use aws_smithy_types::error::metadata::{Builder as ErrorMetadataBuilder, ErrorMetadata};
19
20static PROTOCOL_ID: ShapeId<'static> = shape_id!("aws.protocols", "restXml");
21
22#[derive(Debug)]
24pub struct AwsRestXmlProtocol {
25 inner: HttpBindingProtocol<XmlCodec>,
26 settings: Arc<XmlCodecSettings>,
27 no_error_wrapping: bool,
29 service_xml_namespace: Option<(String, Option<String>)>,
34}
35
36impl AwsRestXmlProtocol {
37 pub fn new() -> Self {
49 let settings = XmlCodecSettings::builder()
50 .default_timestamp_format(TimestampFormat::DateTime)
51 .build();
52 let settings = Arc::new(settings);
53 let codec = XmlCodec::from_shared_settings(settings.clone());
54 Self {
55 inner: HttpBindingProtocol::new(PROTOCOL_ID.clone(), codec, "application/xml"),
56 settings,
57 no_error_wrapping: false,
58 service_xml_namespace: None,
59 }
60 }
61
62 pub fn with_no_error_wrapping(mut self, v: bool) -> Self {
78 self.no_error_wrapping = v;
79 self
80 }
81
82 pub fn no_error_wrapping(&self) -> bool {
85 self.no_error_wrapping
86 }
87
88 pub fn with_service_xml_namespace(
99 mut self,
100 uri: impl Into<String>,
101 prefix: Option<String>,
102 ) -> Self {
103 self.service_xml_namespace = Some((uri.into(), prefix));
104 self
105 }
106
107 fn resolved_service_xml_namespace(&self, cfg: &ConfigBag) -> Option<(String, Option<String>)> {
113 if let Some((uri, prefix)) = &self.service_xml_namespace {
114 return Some((uri.clone(), prefix.clone()));
115 }
116 cfg.load::<aws_smithy_schema::protocol::ServiceXmlNamespace>()
117 .map(|ns| (ns.uri().to_owned(), ns.prefix().map(str::to_owned)))
118 }
119}
120
121pub fn find_error_element_slice(body: &[u8]) -> &[u8] {
139 crate::codec::find_depth2_element_slice_by(body, |name| name == "Error").unwrap_or(body)
144}
145
146impl Default for AwsRestXmlProtocol {
147 fn default() -> Self {
148 Self::new()
149 }
150}
151
152impl aws_smithy_schema::protocol::ClientProtocolInner for AwsRestXmlProtocol {
153 type Request = aws_smithy_runtime_api::http::Request;
154 type Response = aws_smithy_runtime_api::http::Response;
155
156 fn protocol_id(&self) -> &ShapeId<'static> {
157 self.inner.protocol_id()
158 }
159
160 fn serialize_request(
161 &self,
162 input: &dyn aws_smithy_schema::serde::SerializableStruct,
163 input_schema: &Schema<'_>,
164 endpoint: &str,
165 cfg: &ConfigBag,
166 ) -> Result<aws_smithy_runtime_api::http::Request, aws_smithy_schema::serde::SerdeError> {
167 let payload_xml_name = input_schema.members().iter().find_map(|m| {
177 if m.http_payload().is_some()
178 && matches!(
179 m.shape_type(),
180 aws_smithy_schema::ShapeType::Structure | aws_smithy_schema::ShapeType::Union
181 )
182 {
183 m.xml_name().map(|n| n.value().to_owned())
184 } else {
185 None
186 }
187 });
188 let mut body = aws_smithy_schema::codec::Codec::create_serializer(self.inner.codec());
189 if let Some(name) = payload_xml_name {
190 body.set_next_root_xml_name(name);
191 }
192 if let Some((uri, prefix)) = self.resolved_service_xml_namespace(cfg) {
197 body.set_next_root_xml_namespace(uri, prefix);
198 }
199 self.inner
200 .serialize_request_with_body(body, input, input_schema, endpoint, cfg)
201 }
202
203 fn deserialize_response<'a>(
204 &self,
205 response: &'a aws_smithy_runtime_api::http::Response,
206 output_schema: &Schema<'_>,
207 cfg: &'a ConfigBag,
208 ) -> Result<
209 Box<dyn aws_smithy_schema::serde::ShapeDeserializer + 'a>,
210 aws_smithy_schema::serde::SerdeError,
211 > {
212 self.inner
213 .deserialize_response(response, output_schema, cfg)
214 }
215
216 fn parse_error_metadata(
236 &self,
237 response: &aws_smithy_runtime_api::http::Response,
238 _cfg: &ConfigBag,
239 ) -> Result<ErrorMetadataBuilder, SerdeError> {
240 parse_error_envelope_metadata(
241 response.body().bytes().unwrap_or(&[]),
242 self.no_error_wrapping,
243 )
244 }
245
246 fn deserialize_error_response<'a>(
260 &self,
261 response: &'a aws_smithy_runtime_api::http::Response,
262 cfg: &'a ConfigBag,
263 ) -> Result<
264 Box<dyn aws_smithy_schema::serde::ShapeDeserializer + 'a>,
265 aws_smithy_schema::serde::SerdeError,
266 > {
267 let body = response.body().bytes().unwrap_or(&[]);
268 let inner = if self.no_error_wrapping {
269 body
270 } else {
271 find_error_element_slice(body)
272 };
273 Ok(http_error_deserializer(
274 XmlDeserializer::new(inner, self.settings.clone()),
275 response,
276 cfg,
277 ))
278 }
279
280 fn payload_codec(&self) -> Option<&dyn aws_smithy_schema::codec::DynCodec> {
281 self.inner.payload_codec()
282 }
283
284 fn event_stream_media_type(&self) -> Option<&str> {
290 Some("application/xml")
291 }
292
293 fn parse_event_stream_error_metadata(
297 &self,
298 payload: &[u8],
299 ) -> Result<ErrorMetadataBuilder, SerdeError> {
300 parse_error_envelope_metadata(payload, self.no_error_wrapping)
301 }
302
303 fn update_endpoint(
304 &self,
305 request: &mut aws_smithy_runtime_api::http::Request,
306 endpoint: &aws_smithy_types::endpoint::Endpoint,
307 cfg: &ConfigBag,
308 ) -> Result<(), aws_smithy_schema::serde::SerdeError> {
309 self.inner.update_endpoint(request, endpoint, cfg)
310 }
311}
312
313fn parse_error_envelope_metadata(
321 body: &[u8],
322 no_error_wrapping: bool,
323) -> Result<ErrorMetadataBuilder, SerdeError> {
324 if body.is_empty() {
331 return Ok(ErrorMetadata::builder());
332 }
333 let body_str =
334 std::str::from_utf8(body).map_err(|e| SerdeError::custom(format!("invalid UTF-8: {e}")))?;
335 let mut doc = Document::new(body_str);
336 let mut root = doc
337 .root_element()
338 .map_err(|e| SerdeError::custom(format!("{e}")))?;
339 let mut builder = ErrorMetadata::builder();
340
341 if no_error_wrapping {
342 while let Some(mut tag) = root.next_tag() {
344 match tag.start_el().local() {
345 "Code" => {
346 builder = builder
347 .code(try_data(&mut tag).map_err(|e| SerdeError::custom(format!("{e}")))?);
348 }
349 "Message" => {
350 builder = builder.message(
351 try_data(&mut tag).map_err(|e| SerdeError::custom(format!("{e}")))?,
352 );
353 }
354 _ => {}
355 }
356 }
357 } else {
358 while let Some(mut tag) = root.next_tag() {
360 match tag.start_el().local() {
361 "Error" => {
362 while let Some(mut error_field) = tag.next_tag() {
363 match error_field.start_el().local() {
364 "Code" => {
365 builder = builder.code(
366 try_data(&mut error_field)
367 .map_err(|e| SerdeError::custom(format!("{e}")))?,
368 );
369 }
370 "Message" => {
371 builder = builder.message(
372 try_data(&mut error_field)
373 .map_err(|e| SerdeError::custom(format!("{e}")))?,
374 );
375 }
376 _ => {}
377 }
378 }
379 }
380 "RequestId" => {
381 builder = builder.custom(
382 "request_id",
383 try_data(&mut tag).map_err(|e| SerdeError::custom(format!("{e}")))?,
384 );
385 }
386 _ => {}
387 }
388 }
389 }
390 Ok(builder)
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use aws_smithy_schema::protocol::ClientProtocolInner;
397 use aws_smithy_schema::serde::{SerializableStruct, ShapeSerializer};
398 use aws_smithy_schema::{shape_id, ShapeType};
399
400 use aws_smithy_schema::traits::HttpTrait;
401
402 static NAME_MEMBER: Schema<'static> =
403 Schema::new_member(shape_id!("test", "Op$name"), ShapeType::String, "name", 0);
404 static OP_SCHEMA: Schema<'static> = Schema::new_struct(
405 shape_id!("test", "OpRequest"),
406 ShapeType::Structure,
407 &[&NAME_MEMBER],
408 )
409 .with_original_name("OpRequest")
410 .with_http(HttpTrait::new("PUT", "/op", None));
411
412 struct TestInput;
413 impl SerializableStruct for TestInput {
414 fn schema(&self) -> &Schema<'_> {
415 &OP_SCHEMA
416 }
417
418 fn serialize_members(
419 &self,
420 ser: &mut dyn ShapeSerializer,
421 ) -> Result<(), aws_smithy_schema::serde::SerdeError> {
422 ser.write_string(&NAME_MEMBER, "Alice")
423 }
424 }
425
426 #[test]
427 fn serialize_request_produces_xml_body() {
428 let protocol = AwsRestXmlProtocol::new();
429 let cfg = ConfigBag::base();
430 let request = protocol
431 .serialize_request(&TestInput, &OP_SCHEMA, "", &cfg)
432 .unwrap();
433
434 assert_eq!(request.method(), "PUT");
435 assert_eq!(request.uri(), "/op");
436 assert_eq!(
437 request.headers().get("content-type").unwrap(),
438 "application/xml"
439 );
440 let body = std::str::from_utf8(request.body().bytes().unwrap()).unwrap();
441 assert_eq!(body, "<OpRequest><name>Alice</name></OpRequest>");
442 }
443
444 #[test]
448 fn service_xml_namespace_defaults_from_config_bag() {
449 let mut layer = aws_smithy_types::config_bag::Layer::new("test");
450 layer.store_put(aws_smithy_schema::protocol::ServiceXmlNamespace::new(
451 "http://example.com/from-bag/",
452 None,
453 ));
454 let cfg = ConfigBag::of_layers(vec![layer]);
455
456 let request = AwsRestXmlProtocol::new()
457 .serialize_request(&TestInput, &OP_SCHEMA, "", &cfg)
458 .unwrap();
459 let body = std::str::from_utf8(request.body().bytes().unwrap()).unwrap();
460 assert_eq!(
461 body,
462 "<OpRequest xmlns=\"http://example.com/from-bag/\"><name>Alice</name></OpRequest>"
463 );
464 }
465
466 #[test]
468 fn service_xml_namespace_from_config_bag_honors_prefix() {
469 let mut layer = aws_smithy_types::config_bag::Layer::new("test");
470 layer.store_put(aws_smithy_schema::protocol::ServiceXmlNamespace::new(
471 "http://example.com/from-bag/",
472 Some("ex".into()),
473 ));
474 let cfg = ConfigBag::of_layers(vec![layer]);
475
476 let request = AwsRestXmlProtocol::new()
477 .serialize_request(&TestInput, &OP_SCHEMA, "", &cfg)
478 .unwrap();
479 let body = std::str::from_utf8(request.body().bytes().unwrap()).unwrap();
480 assert_eq!(
481 body,
482 "<OpRequest xmlns:ex=\"http://example.com/from-bag/\"><name>Alice</name></OpRequest>"
483 );
484 }
485
486 #[test]
488 fn with_service_xml_namespace_overrides_config_bag() {
489 let mut layer = aws_smithy_types::config_bag::Layer::new("test");
490 layer.store_put(aws_smithy_schema::protocol::ServiceXmlNamespace::new(
491 "http://example.com/from-bag/",
492 None,
493 ));
494 let cfg = ConfigBag::of_layers(vec![layer]);
495
496 let request = AwsRestXmlProtocol::new()
497 .with_service_xml_namespace("http://example.com/explicit/".to_owned(), None)
498 .serialize_request(&TestInput, &OP_SCHEMA, "", &cfg)
499 .unwrap();
500 let body = std::str::from_utf8(request.body().bytes().unwrap()).unwrap();
501 assert_eq!(
502 body,
503 "<OpRequest xmlns=\"http://example.com/explicit/\"><name>Alice</name></OpRequest>"
504 );
505 }
506
507 #[test]
508 fn deserialize_response_round_trips() {
509 let protocol = AwsRestXmlProtocol::new();
510 let cfg = ConfigBag::base();
511 let body = b"<OpResponse><name>Bob</name></OpResponse>";
512 let response = aws_smithy_runtime_api::http::Response::new(
513 aws_smithy_runtime_api::http::StatusCode::try_from(200).unwrap(),
514 aws_smithy_types::body::SdkBody::from(&body[..]),
515 );
516
517 let mut deser = protocol
518 .deserialize_response(&response, &OP_SCHEMA, &cfg)
519 .unwrap();
520
521 let mut name = String::new();
522 deser
523 .read_struct(&OP_SCHEMA, &mut |member, d| {
524 if member.member_name() == Some("name") {
525 name = d.read_string(member)?;
526 }
527 Ok(())
528 })
529 .unwrap();
530 assert_eq!(name, "Bob");
531 }
532
533 fn http_response(body: &[u8]) -> aws_smithy_runtime_api::http::Response {
534 aws_smithy_runtime_api::http::Response::new(
535 aws_smithy_runtime_api::http::StatusCode::try_from(400).unwrap(),
536 aws_smithy_types::body::SdkBody::from(body),
537 )
538 }
539
540 #[test]
541 fn protocol_id_is_rest_xml() {
542 assert_eq!(
543 AwsRestXmlProtocol::new().protocol_id().as_str(),
544 "aws.protocols#restXml"
545 );
546 }
547
548 #[test]
549 fn parse_error_metadata_wrapped() {
550 let protocol = AwsRestXmlProtocol::new();
551 let response = http_response(b"<ErrorResponse><Error><Type>Sender</Type><Code>InvalidGreeting</Code><Message>Hi</Message><Greeting>Howdy</Greeting></Error><RequestId>req-1</RequestId></ErrorResponse>");
552 let cfg = ConfigBag::base();
553
554 let meta = protocol
555 .parse_error_metadata(&response, &cfg)
556 .unwrap()
557 .build();
558 assert_eq!(meta.code(), Some("InvalidGreeting"));
559 assert_eq!(meta.message(), Some("Hi"));
560 assert_eq!(meta.extra("request_id"), Some("req-1"));
561 }
562
563 #[test]
564 fn parse_error_metadata_unwrapped() {
565 let protocol = AwsRestXmlProtocol::new().with_no_error_wrapping(true);
566 let response = http_response(
567 b"<Error><Code>NotFound</Code><Message>Gone</Message><Detail>extra</Detail></Error>",
568 );
569 let cfg = ConfigBag::base();
570
571 let meta = protocol
572 .parse_error_metadata(&response, &cfg)
573 .unwrap()
574 .build();
575 assert_eq!(meta.code(), Some("NotFound"));
576 assert_eq!(meta.message(), Some("Gone"));
577 assert_eq!(meta.extra("request_id"), None);
579 }
580
581 #[test]
582 fn parse_error_metadata_wrapped_ignores_literal_error_in_cdata() {
583 let protocol = AwsRestXmlProtocol::new();
590 let response = http_response(
591 b"<ErrorResponse>\
592 <Description><![CDATA[<Error><Code>FAKE</Code></Error>]]></Description>\
593 <Error><Code>RealCode</Code><Message>Real message</Message></Error>\
594 <RequestId>req-1</RequestId>\
595 </ErrorResponse>",
596 );
597 let cfg = ConfigBag::base();
598
599 let meta = protocol
600 .parse_error_metadata(&response, &cfg)
601 .unwrap()
602 .build();
603 assert_eq!(meta.code(), Some("RealCode"));
606 assert_eq!(meta.message(), Some("Real message"));
607 assert_eq!(meta.extra("request_id"), Some("req-1"));
608 }
609
610 #[test]
611 fn deserialize_error_response_positions_inside_error_wrapped() {
612 let protocol = AwsRestXmlProtocol::new();
616 let response = http_response(
617 b"<ErrorResponse>\
618 <Error xmlns=\"http://example.com/\"><name>Carol</name></Error>\
619 <RequestId>req-1</RequestId>\
620 </ErrorResponse>",
621 );
622 let cfg = ConfigBag::base();
623
624 let mut deser = protocol
625 .deserialize_error_response(&response, &cfg)
626 .unwrap();
627
628 let mut name = String::new();
629 deser
630 .read_struct(&OP_SCHEMA, &mut |member, d| {
631 if member.member_name() == Some("name") {
632 name = d.read_string(member)?;
633 }
634 Ok(())
635 })
636 .unwrap();
637 assert_eq!(name, "Carol");
638 }
639
640 #[test]
641 fn deserialize_error_response_positions_inside_error_unwrapped() {
642 let protocol = AwsRestXmlProtocol::new().with_no_error_wrapping(true);
645 let response = http_response(b"<Error><name>Dave</name></Error>");
646 let cfg = ConfigBag::base();
647
648 let mut deser = protocol
649 .deserialize_error_response(&response, &cfg)
650 .unwrap();
651
652 let mut name = String::new();
653 deser
654 .read_struct(&OP_SCHEMA, &mut |member, d| {
655 if member.member_name() == Some("name") {
656 name = d.read_string(member)?;
657 }
658 Ok(())
659 })
660 .unwrap();
661 assert_eq!(name, "Dave");
662 }
663
664 #[test]
668 fn find_error_element_slice_strips_wrapped_envelope() {
669 let body =
670 b"<ErrorResponse><Error><Code>X</Code></Error><RequestId>r</RequestId></ErrorResponse>";
671 let slice = find_error_element_slice(body);
672 assert_eq!(slice, b"<Error><Code>X</Code></Error>");
673 }
674
675 #[test]
676 fn find_error_element_slice_handles_xmlns_on_inner_error() {
677 let body = br#"<ErrorResponse><Error xmlns="http://example.com/"><Code>X</Code></Error></ErrorResponse>"#;
682 let slice = find_error_element_slice(body);
683 assert_eq!(
684 slice,
685 br#"<Error xmlns="http://example.com/"><Code>X</Code></Error>"#
686 );
687 }
688
689 #[test]
690 fn find_error_element_slice_returns_body_when_root_is_error() {
691 let body = b"<Error><Code>X</Code><Message>m</Message></Error>";
695 let slice = find_error_element_slice(body);
696 assert_eq!(slice, body);
697 }
698
699 #[test]
700 fn find_error_element_slice_falls_back_for_missing_error() {
701 let body = b"<SomethingElse><Code>X</Code></SomethingElse>";
705 let slice = find_error_element_slice(body);
706 assert_eq!(slice, body);
707 }
708
709 #[test]
710 fn find_error_element_slice_falls_back_for_invalid_xml() {
711 let body = b"not xml at all";
713 let slice = find_error_element_slice(body);
714 assert_eq!(slice, body);
715 }
716
717 #[test]
718 fn find_error_element_slice_falls_back_for_invalid_utf8() {
719 let body = &[0xFFu8, 0xFE, 0xFD][..];
722 let slice = find_error_element_slice(body);
723 assert_eq!(slice, body);
724 }
725
726 static ERR_REASON: Schema<'static> = Schema::new_member(
731 shape_id!("test", "Err$reason"),
732 ShapeType::String,
733 "reason",
734 0,
735 )
736 .with_http_header("x-reason");
737 static ERR_STATUS: Schema<'static> = Schema::new_member(
738 shape_id!("test", "Err$status"),
739 ShapeType::Integer,
740 "status",
741 1,
742 )
743 .with_http_response_code();
744 static ERR_NAME: Schema<'static> =
745 Schema::new_member(shape_id!("test", "Err$name"), ShapeType::String, "name", 2);
746 static ERR_SCHEMA: Schema<'static> = Schema::new_struct(
747 shape_id!("test", "Err"),
748 ShapeType::Structure,
749 &[&ERR_REASON, &ERR_STATUS, &ERR_NAME],
750 );
751
752 fn http_response_with_header(
753 name: &str,
754 value: &str,
755 body: &[u8],
756 ) -> aws_smithy_runtime_api::http::Response {
757 let mut response = http_response(body);
758 response
759 .headers_mut()
760 .insert(name.to_string(), value.to_string());
761 response
762 }
763
764 #[test]
765 fn a_modeled_error_reads_its_header_and_its_body_member_from_the_envelope() {
766 let protocol = AwsRestXmlProtocol::new();
770 let response = http_response_with_header(
771 "x-reason",
772 "throttled",
773 b"<ErrorResponse>\
774 <Error><name>Carol</name></Error>\
775 <RequestId>req-1</RequestId>\
776 </ErrorResponse>",
777 );
778 let cfg = ConfigBag::base();
779
780 let mut reason = None;
781 let mut status = None;
782 let mut name = None;
783 protocol
784 .deserialize_error_response(&response, &cfg)
785 .unwrap()
786 .read_struct(&ERR_SCHEMA, &mut |member, d| {
787 match member.member_name() {
788 Some("reason") => reason = Some(d.read_string(member)?),
789 Some("status") => status = Some(d.read_integer(member)?),
790 Some("name") => name = Some(d.read_string(member)?),
791 _ => {}
792 }
793 Ok(())
794 })
795 .unwrap();
796
797 assert_eq!(
798 reason.as_deref(),
799 Some("throttled"),
800 "the header-bound member must come from the response"
801 );
802 assert_eq!(status, Some(400), "the status must come from the response");
803 assert_eq!(
804 name.as_deref(),
805 Some("Carol"),
806 "the body member must still be read from inside <Error>"
807 );
808 }
809}