Skip to main content

aws_smithy_xml/protocol/
aws_rest_xml.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! AWS REST XML protocol implementation (`aws.protocols#restXml`).
7
8use 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/// AWS REST XML protocol (`aws.protocols#restXml`).
23#[derive(Debug)]
24pub struct AwsRestXmlProtocol {
25    inner: HttpBindingProtocol<XmlCodec>,
26    settings: Arc<XmlCodecSettings>,
27    /// True if the service has `@restXml(noErrorWrapping: true)`.
28    no_error_wrapping: bool,
29    /// Service-level `@xmlNamespace` URI/prefix. Per the Smithy spec, this is
30    /// the default xmlns applied to operation request/response root elements
31    /// when the operation's input/output struct (or its `@httpPayload` target)
32    /// doesn't declare its own `@xmlNamespace`.
33    service_xml_namespace: Option<(String, Option<String>)>,
34}
35
36impl AwsRestXmlProtocol {
37    /// Construct a new `AwsRestXmlProtocol` with default settings:
38    /// Content-Type `application/xml`, `date-time` default timestamp
39    /// format, error envelopes wrapped in `<ErrorResponse>` (i.e.
40    /// `noErrorWrapping = false`), and no service-level `@xmlNamespace`.
41    ///
42    /// Use [`Self::with_no_error_wrapping`] to opt in to the `noErrorWrapping`
43    /// variant of the `@restXml` trait, and
44    /// [`Self::with_service_xml_namespace`] to set a service-level default
45    /// xmlns. Both methods are typically called by code generation based on
46    /// the service shape's traits; manual construction is fine for tests
47    /// or custom protocols.
48    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    /// Configure whether error responses use the `noErrorWrapping` variant
63    /// of the `@restXml` trait. When `true`, error response bodies have
64    /// `<Error>` as the document root; when `false` (default), they are
65    /// wrapped in `<ErrorResponse><Error>...</Error>...</ErrorResponse>`.
66    /// Drives the parsing branch in `parse_error_metadata` and
67    /// `deserialize_error_response`.
68    ///
69    /// Unlike the service XML namespace, this has **no config-bag default and cannot have one**:
70    /// it is read from `@restXml(noErrorWrapping)`, and a model that does not use restXml carries
71    /// no `@restXml` trait for a generated client to store. So when restXml is selected at runtime
72    /// on, say, a CBOR-modeled service, there is nothing to recover — the caller must set it, and
73    /// the documented default of `false` (wrapped envelopes, the spec default) applies otherwise.
74    ///
75    /// This is the general limit of runtime protocol swapping: a protocol's own trait data cannot
76    /// be recovered from a model that does not use that protocol.
77    pub fn with_no_error_wrapping(mut self, v: bool) -> Self {
78        self.no_error_wrapping = v;
79        self
80    }
81
82    /// Returns the current `noErrorWrapping` setting. See
83    /// [`Self::with_no_error_wrapping`] for semantics.
84    pub fn no_error_wrapping(&self) -> bool {
85        self.no_error_wrapping
86    }
87
88    /// Configures the service-level `@xmlNamespace` declared on the Smithy
89    /// service shape. Applied to request/response XML root elements that
90    /// don't carry their own `@xmlNamespace` (e.g. S3's
91    /// `http://s3.amazonaws.com/doc/2006-03-01/`).
92    ///
93    /// Overrides the default, which is the
94    /// [`ServiceXmlNamespace`](aws_smithy_schema::protocol::ServiceXmlNamespace) config-bag entry
95    /// that generated clients store regardless of which protocol they were generated for. That
96    /// default exists because a customer selecting restXml through
97    /// `Config::builder().protocol(..)` has no way to know the model's namespace.
98    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    /// Resolves the service-level `@xmlNamespace`: an explicit configuration wins, otherwise the
108    /// config-bag entry.
109    ///
110    /// Returns owned values because the codec takes ownership; the explicit branch clones exactly
111    /// as it did before this fell back to the bag.
112    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
121/// Locate the `<Error>` element within an AWS REST XML error response body
122/// and return a byte slice covering it (`<Error>...</Error>`).
123///
124/// Handles both wrapped and unwrapped error envelopes:
125/// - **Wrapped** (`<ErrorResponse>...<Error>...</Error>...</ErrorResponse>`):
126///   returns the inner `<Error>` element's slice.
127/// - **Unwrapped** (`<Error>...</Error>` as the document root): returns the
128///   full body unchanged (the root *is* the `<Error>` element).
129///
130/// Falls back to the full `body` if it isn't valid UTF-8, isn't parseable as
131/// XML, or contains no `<Error>` element. Returning the body unchanged on
132/// failure lets downstream error deserialization surface a generic /
133/// "unhandled" error variant rather than panicking on malformed responses.
134///
135/// Robust to start-tag attributes (e.g. `<Error xmlns="..."`), nested
136/// same-name elements, comments, and CDATA sections — all of which a naive
137/// `body_str.find("<Error>")` substring match would mishandle.
138pub fn find_error_element_slice(body: &[u8]) -> &[u8] {
139    // Depth-2 `<Error>` (wrapped) or the root itself (unwrapped). Fall back to
140    // the full body when the response isn't valid UTF-8/XML or has no `<Error>`,
141    // so downstream parsing produces a malformed-error result rather than
142    // panicking.
143    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        // XML-specific pre-scan: if the input has an `@httpPayload` struct or
168        // union member with its own `@xmlName`, the body's wrapper element
169        // must be that name. Codegen passes the *target* shape's `SCHEMA`
170        // for the payload member's `write_struct` call (so the codec sees
171        // the target's `@xmlName`, not the member's), so the codec on its
172        // own would emit the wrong wrapper. Look up the member here, where
173        // we have the input schema in hand, and pre-set the override on the
174        // body serializer; the XmlSerializer consumes it on the first
175        // root-level `write_struct`.
176        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        // Apply service-level `@xmlNamespace` as the document-root xmlns
193        // fallback. Consumed by the codec on the first root-level
194        // `write_struct` only if the struct's schema has no own
195        // `@xmlNamespace`.
196        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    /// Extracts canonical error metadata from a REST XML response.
217    ///
218    /// Walks the error envelope structurally to extract `Code` / `Message` /
219    /// `Type` from inside `<Error>` and (for the wrapped variant) `RequestId`
220    /// from the outer `<ErrorResponse>`.
221    ///
222    /// Honors [`Self::with_no_error_wrapping`]:
223    ///
224    /// - **Wrapped** (default): expects
225    ///   `<ErrorResponse><Error><Code>...</Code>...</Error><RequestId>...</RequestId></ErrorResponse>`.
226    /// - **Unwrapped** (`@restXml(noErrorWrapping: true)`): expects
227    ///   `<Error><Code>...</Code>...</Error>` as the document root.
228    ///
229    /// Per the
230    /// [`ClientProtocolInner::parse_error_metadata`](aws_smithy_schema::protocol::ClientProtocolInner::parse_error_metadata)
231    /// contract the request id is **not** populated here — the
232    /// orchestrator's request-id pipeline attaches it separately. The
233    /// `RequestId` element is captured into the builder's `extra` slot
234    /// under the key `request_id` for callers that need it.
235    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    /// Returns a [`ShapeDeserializer`](aws_smithy_schema::serde::ShapeDeserializer)
247    /// positioned inside `<Error>` so generated
248    /// `SpecificError::deserialize` reads land on the right elements.
249    ///
250    /// For the wrapped variant, uses [`find_error_element_slice`] to locate
251    /// the `<Error>` sub-element. For `noErrorWrapping`, the body root IS
252    /// `<Error>`, so the body is used unchanged.
253    ///
254    /// The positioned deserializer is then wrapped so the error's modeled `@httpHeader`,
255    /// `@httpPrefixHeaders`, and `@httpResponseCode` members are read from the HTTP message.
256    /// Envelope positioning and binding routing are independent: the codec sees the `<Error>`
257    /// sub-slice, while the wrapper reads the response's headers and status. This is why the
258    /// wrapping helper takes an already-prepared body rather than building one.
259    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    /// This protocol labels structured event-stream payloads `application/xml`.
285    ///
286    /// Must stay in agreement with the code generator's
287    /// `eventStreamMessageContentType` for this protocol (`RestXml.kt:47`), which supplies
288    /// the fallback when a protocol declares no media type.
289    fn event_stream_media_type(&self) -> Option<&str> {
290        Some("application/xml")
291    }
292
293    /// Parses the same restXml error envelope as
294    /// [`ClientProtocolInner::parse_error_metadata`](aws_smithy_schema::protocol::ClientProtocolInner::parse_error_metadata), from an event-stream
295    /// frame's payload rather than an HTTP response body.
296    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
313/// Parses a restXml error envelope out of a response or event-stream payload.
314///
315/// Shared by [`ClientProtocolInner::parse_error_metadata`](aws_smithy_schema::protocol::ClientProtocolInner::parse_error_metadata) and
316/// [`ClientProtocolInner::parse_event_stream_error_metadata`](aws_smithy_schema::protocol::ClientProtocolInner::parse_event_stream_error_metadata) so the two cannot
317/// drift. Takes `no_error_wrapping` rather than `&self` because it needs no other
318/// protocol state, and takes the bytes rather than a response because an
319/// event-stream frame is not one.
320fn parse_error_envelope_metadata(
321    body: &[u8],
322    no_error_wrapping: bool,
323) -> Result<ErrorMetadataBuilder, SerdeError> {
324    // An empty body carries no error metadata. Some services signal errors
325    // with a status code and an empty body (e.g. S3 HEAD operations, whose
326    // responses have no body to hold an error code). Return an empty builder
327    // rather than failing to locate a root XML element; the error code for
328    // such responses is derived from the HTTP status by the generated
329    // dispatch's error-metadata customizations.
330    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        // <Error><Code>...</Code><Message>...</Message>...members...</Error>
343        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        // <ErrorResponse><Error><Code>...</Code>...</Error><RequestId>...</RequestId></ErrorResponse>
359        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    /// The service-level `@xmlNamespace` becomes the root `xmlns`, and a customer selecting restXml
445    /// through `Config::builder().protocol(..)` has no way to know it, so it defaults from the
446    /// config-bag entry generated clients store regardless of their protocol.
447    #[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    /// A prefix in the bag entry is honored too.
467    #[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    /// An explicitly configured namespace still wins over the bag.
487    #[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        // No outer envelope means no RequestId on the unwrapped path.
578        assert_eq!(meta.extra("request_id"), None);
579    }
580
581    #[test]
582    fn parse_error_metadata_wrapped_ignores_literal_error_in_cdata() {
583        // Regression: a previous implementation used
584        // `body_str.find("<Error>")` to locate the envelope's `<Error>`
585        // sub-element, which would also match the literal bytes of
586        // `<Error>` inside an unrelated CDATA section. The new
587        // structural walk uses the XML parser, which treats CDATA as
588        // opaque text and only matches a real `<Error>` element.
589        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        // The structural walk skips the CDATA content and finds the
604        // real <Error>'s <Code>.
605        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        // The deserializer returned by the trait method must be
613        // positioned over the `<Error>` element so generated
614        // `SpecificError::deserialize` reads its members directly.
615        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        // For `noErrorWrapping`, the body root IS `<Error>` so the
643        // deserializer reads from the body unchanged.
644        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    // Regression tests for `find_error_element_slice` covering the cases
665    // where a naive `body.find("<Error>")` substring match would
666    // mishandle the input.
667    #[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        // The exact case the substring-find fails on: `xmlns` directly
678        // on the inner `<Error>` start tag means `body.find("<Error>")`
679        // returns `None` and the previous codegen returned the full
680        // body, breaking downstream error-code lookup.
681        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        // `noErrorWrapping` mode: the root element IS `<Error>`. The
692        // slice should be the full body, since the root's tags are
693        // already at the boundaries.
694        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        // Defensive: if the body doesn't contain an `<Error>` element
702        // anywhere, return the body unchanged so downstream parsing
703        // produces a malformed/unhandled error rather than panicking.
704        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        // Defensive: malformed XML returns the body unchanged.
712        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        // Defensive: non-UTF-8 bytes return the body unchanged. The
720        // `Document::try_from(&[u8])` path validates UTF-8.
721        let body = &[0xFFu8, 0xFE, 0xFD][..];
722        let slice = find_error_element_slice(body);
723        assert_eq!(slice, body);
724    }
725
726    // ---------------------------------------------------------------------------------
727    // envelope positioning and binding routing compose
728    // ---------------------------------------------------------------------------------
729
730    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        // The two concerns are independent and must both hold: the codec sees the `<Error>`
767        // sub-slice, while the wrapper reads the response's own headers and status. Before the
768        // error path was wrapped, a modeled error's header-bound members were silently absent.
769        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}