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::HttpBindingProtocol;
13use aws_smithy_schema::serde::SerdeError;
14use aws_smithy_schema::Schema;
15use aws_smithy_schema::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 = ShapeId::from_static("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, 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 [`Self::deserialize_error_response`].
67    pub fn with_no_error_wrapping(mut self, v: bool) -> Self {
68        self.no_error_wrapping = v;
69        self
70    }
71
72    /// Returns the current `noErrorWrapping` setting. See
73    /// [`Self::with_no_error_wrapping`] for semantics.
74    pub fn no_error_wrapping(&self) -> bool {
75        self.no_error_wrapping
76    }
77
78    /// Configures the service-level `@xmlNamespace` declared on the Smithy
79    /// service shape. Applied to request/response XML root elements that
80    /// don't carry their own `@xmlNamespace` (e.g. S3's
81    /// `http://s3.amazonaws.com/doc/2006-03-01/`).
82    pub fn with_service_xml_namespace(
83        mut self,
84        uri: impl Into<String>,
85        prefix: Option<String>,
86    ) -> Self {
87        self.service_xml_namespace = Some((uri.into(), prefix));
88        self
89    }
90
91    /// Parses a REST XML error response envelope, extracting error metadata
92    /// (`Code`, `Message`, `Type`, `RequestId`) and returning a deserializer
93    /// positioned inside `<Error>` for per-error-shape member parsing.
94    pub fn deserialize_error_response<'a>(
95        &self,
96        body: &'a [u8],
97    ) -> Result<
98        (
99            ErrorMetadataBuilder,
100            Box<dyn aws_smithy_schema::serde::ShapeDeserializer + 'a>,
101        ),
102        SerdeError,
103    > {
104        let mut builder = ErrorMetadata::builder();
105
106        if self.no_error_wrapping {
107            // <Error><Code>...</Code><Message>...</Message>...members...</Error>
108            let mut doc = Document::new(
109                std::str::from_utf8(body)
110                    .map_err(|e| SerdeError::custom(format!("invalid UTF-8: {e}")))?,
111            );
112            let mut root = doc
113                .root_element()
114                .map_err(|e| SerdeError::custom(format!("{e}")))?;
115            while let Some(mut tag) = root.next_tag() {
116                match tag.start_el().local() {
117                    "Code" => {
118                        builder = builder.code(
119                            try_data(&mut tag).map_err(|e| SerdeError::custom(format!("{e}")))?,
120                        );
121                    }
122                    "Message" => {
123                        builder = builder.message(
124                            try_data(&mut tag).map_err(|e| SerdeError::custom(format!("{e}")))?,
125                        );
126                    }
127                    _ => {}
128                }
129            }
130            // For unwrapped, the body IS the <Error> element — deserializer reads from root
131            let deser = XmlDeserializer::new(body, self.settings.clone());
132            Ok((builder, Box::new(deser)))
133        } else {
134            // <ErrorResponse><Error><Code>...</Code>...</Error><RequestId>...</RequestId></ErrorResponse>
135            let mut doc = Document::new(
136                std::str::from_utf8(body)
137                    .map_err(|e| SerdeError::custom(format!("invalid UTF-8: {e}")))?,
138            );
139            let mut root = doc
140                .root_element()
141                .map_err(|e| SerdeError::custom(format!("{e}")))?;
142            // Captured during the structural walk below — bytes of the
143            // `<Error>...</Error>` sub-element. Replaces a previous
144            // `body_str.find("<Error>")` substring search that would
145            // match `<Error>` literally inside attribute values, CDATA
146            // sections, comments, or text content. The structural walk
147            // already locates the element correctly via the XML parser;
148            // capturing the slice during the walk reuses that work and
149            // matches only an actual `<Error>` element.
150            let mut error_fragment: Option<&'a [u8]> = None;
151            while let Some(mut tag) = root.next_tag() {
152                match tag.start_el().local() {
153                    "Error" => {
154                        // `local()` returns a `&str` borrowed from the
155                        // input bytes — exactly what `find_element_slice`
156                        // expects (its pointer-arithmetic invariant
157                        // requires the name to lie within `body`).
158                        let el_local = tag.start_el().local();
159                        error_fragment = Some(XmlDeserializer::find_element_slice(body, el_local));
160                        while let Some(mut error_field) = tag.next_tag() {
161                            match error_field.start_el().local() {
162                                "Code" => {
163                                    builder = builder.code(
164                                        try_data(&mut error_field)
165                                            .map_err(|e| SerdeError::custom(format!("{e}")))?,
166                                    );
167                                }
168                                "Message" => {
169                                    builder = builder.message(
170                                        try_data(&mut error_field)
171                                            .map_err(|e| SerdeError::custom(format!("{e}")))?,
172                                    );
173                                }
174                                _ => {}
175                            }
176                        }
177                    }
178                    "RequestId" => {
179                        builder = builder.custom(
180                            "request_id",
181                            try_data(&mut tag).map_err(|e| SerdeError::custom(format!("{e}")))?,
182                        );
183                    }
184                    _ => {}
185                }
186            }
187            // The deserializer that downstream error-shape parsing reads
188            // from must see `<Error>` as its root element. If we found
189            // one, point at that fragment; otherwise the body is malformed
190            // and the fallback to the whole body lets downstream parsing
191            // produce an "unknown error" rather than a panic.
192            let deser = match error_fragment {
193                Some(fragment) => XmlDeserializer::new(fragment, self.settings.clone()),
194                None => XmlDeserializer::new(body, self.settings.clone()),
195            };
196            Ok((builder, Box::new(deser)))
197        }
198    }
199}
200
201/// Locate the `<Error>` element within an AWS REST XML error response body
202/// and return a byte slice covering it (`<Error>...</Error>`).
203///
204/// Handles both wrapped and unwrapped error envelopes:
205/// - **Wrapped** (`<ErrorResponse>...<Error>...</Error>...</ErrorResponse>`):
206///   returns the inner `<Error>` element's slice.
207/// - **Unwrapped** (`<Error>...</Error>` as the document root): returns the
208///   full body unchanged (the root *is* the `<Error>` element).
209///
210/// Falls back to the full `body` if it isn't valid UTF-8, isn't parseable as
211/// XML, or contains no `<Error>` element. Returning the body unchanged on
212/// failure lets downstream error deserialization surface a generic /
213/// "unhandled" error variant rather than panicking on malformed responses.
214///
215/// Robust to start-tag attributes (e.g. `<Error xmlns="..."`), nested
216/// same-name elements, comments, and CDATA sections — all of which a naive
217/// `body_str.find("<Error>")` substring match would mishandle.
218pub fn find_error_element_slice(body: &[u8]) -> &[u8] {
219    // Depth-2 `<Error>` (wrapped) or the root itself (unwrapped). Fall back to
220    // the full body when the response isn't valid UTF-8/XML or has no `<Error>`,
221    // so downstream parsing produces a malformed-error result rather than
222    // panicking.
223    crate::codec::find_depth2_element_slice_by(body, |name| name == "Error").unwrap_or(body)
224}
225
226impl Default for AwsRestXmlProtocol {
227    fn default() -> Self {
228        Self::new()
229    }
230}
231
232impl aws_smithy_schema::protocol::ClientProtocolInner for AwsRestXmlProtocol {
233    type Request = aws_smithy_runtime_api::http::Request;
234    type Response = aws_smithy_runtime_api::http::Response;
235
236    fn protocol_id(&self) -> &ShapeId {
237        self.inner.protocol_id()
238    }
239
240    fn serialize_request(
241        &self,
242        input: &dyn aws_smithy_schema::serde::SerializableStruct,
243        input_schema: &Schema,
244        endpoint: &str,
245        cfg: &ConfigBag,
246    ) -> Result<aws_smithy_runtime_api::http::Request, aws_smithy_schema::serde::SerdeError> {
247        // XML-specific pre-scan: if the input has an `@httpPayload` struct or
248        // union member with its own `@xmlName`, the body's wrapper element
249        // must be that name. Codegen passes the *target* shape's `SCHEMA`
250        // for the payload member's `write_struct` call (so the codec sees
251        // the target's `@xmlName`, not the member's), so the codec on its
252        // own would emit the wrong wrapper. Look up the member here, where
253        // we have the input schema in hand, and pre-set the override on the
254        // body serializer; the XmlSerializer consumes it on the first
255        // root-level `write_struct`.
256        let payload_xml_name = input_schema.members().iter().find_map(|m| {
257            if m.http_payload().is_some()
258                && matches!(
259                    m.shape_type(),
260                    aws_smithy_schema::ShapeType::Structure | aws_smithy_schema::ShapeType::Union
261                )
262            {
263                m.xml_name().map(|n| n.value().to_owned())
264            } else {
265                None
266            }
267        });
268        let mut body = aws_smithy_schema::codec::Codec::create_serializer(self.inner.codec());
269        if let Some(name) = payload_xml_name {
270            body.set_next_root_xml_name(name);
271        }
272        // Apply service-level `@xmlNamespace` as the document-root xmlns
273        // fallback. Consumed by the codec on the first root-level
274        // `write_struct` only if the struct's schema has no own
275        // `@xmlNamespace`.
276        if let Some((uri, prefix)) = &self.service_xml_namespace {
277            body.set_next_root_xml_namespace(uri.clone(), prefix.clone());
278        }
279        self.inner
280            .serialize_request_with_body(body, input, input_schema, endpoint, cfg)
281    }
282
283    fn deserialize_response<'a>(
284        &self,
285        response: &'a aws_smithy_runtime_api::http::Response,
286        output_schema: &Schema,
287        cfg: &ConfigBag,
288    ) -> Result<
289        Box<dyn aws_smithy_schema::serde::ShapeDeserializer + 'a>,
290        aws_smithy_schema::serde::SerdeError,
291    > {
292        self.inner
293            .deserialize_response(response, output_schema, cfg)
294    }
295
296    fn payload_codec(&self) -> Option<&dyn aws_smithy_schema::codec::DynCodec> {
297        self.inner.payload_codec()
298    }
299
300    fn update_endpoint(
301        &self,
302        request: &mut aws_smithy_runtime_api::http::Request,
303        endpoint: &aws_smithy_types::endpoint::Endpoint,
304        cfg: &ConfigBag,
305    ) -> Result<(), aws_smithy_schema::serde::SerdeError> {
306        self.inner.update_endpoint(request, endpoint, cfg)
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use aws_smithy_schema::protocol::ClientProtocolInner;
314    use aws_smithy_schema::serde::{SerializableStruct, ShapeSerializer};
315    use aws_smithy_schema::{shape_id, ShapeType};
316
317    use aws_smithy_schema::traits::HttpTrait;
318
319    static NAME_MEMBER: Schema =
320        Schema::new_member(shape_id!("test", "Op$name"), ShapeType::String, "name", 0);
321    static OP_SCHEMA: Schema = Schema::new_struct(
322        shape_id!("test", "OpRequest"),
323        ShapeType::Structure,
324        &[&NAME_MEMBER],
325    )
326    .with_original_name("OpRequest")
327    .with_http(HttpTrait::new("PUT", "/op", None));
328
329    struct TestInput;
330    impl SerializableStruct for TestInput {
331        fn serialize_members(
332            &self,
333            ser: &mut dyn ShapeSerializer,
334        ) -> Result<(), aws_smithy_schema::serde::SerdeError> {
335            ser.write_string(&NAME_MEMBER, "Alice")
336        }
337    }
338
339    #[test]
340    fn serialize_request_produces_xml_body() {
341        let protocol = AwsRestXmlProtocol::new();
342        let cfg = ConfigBag::base();
343        let request = protocol
344            .serialize_request(&TestInput, &OP_SCHEMA, "", &cfg)
345            .unwrap();
346
347        assert_eq!(request.method(), "PUT");
348        assert_eq!(request.uri(), "/op");
349        assert_eq!(
350            request.headers().get("content-type").unwrap(),
351            "application/xml"
352        );
353        let body = std::str::from_utf8(request.body().bytes().unwrap()).unwrap();
354        assert_eq!(body, "<OpRequest><name>Alice</name></OpRequest>");
355    }
356
357    #[test]
358    fn deserialize_response_round_trips() {
359        let protocol = AwsRestXmlProtocol::new();
360        let cfg = ConfigBag::base();
361        let body = b"<OpResponse><name>Bob</name></OpResponse>";
362        let response = aws_smithy_runtime_api::http::Response::new(
363            aws_smithy_runtime_api::http::StatusCode::try_from(200).unwrap(),
364            aws_smithy_types::body::SdkBody::from(&body[..]),
365        );
366
367        let mut deser = protocol
368            .deserialize_response(&response, &OP_SCHEMA, &cfg)
369            .unwrap();
370
371        let mut name = String::new();
372        deser
373            .read_struct(&OP_SCHEMA, &mut |member, d| {
374                if member.member_name() == Some("name") {
375                    name = d.read_string(member)?;
376                }
377                Ok(())
378            })
379            .unwrap();
380        assert_eq!(name, "Bob");
381    }
382
383    #[test]
384    fn deserialize_error_wrapped() {
385        let protocol = AwsRestXmlProtocol::new();
386        let body = b"<ErrorResponse><Error><Type>Sender</Type><Code>InvalidGreeting</Code><Message>Hi</Message><Greeting>Howdy</Greeting></Error><RequestId>req-1</RequestId></ErrorResponse>";
387
388        let (builder, mut deser) = protocol.deserialize_error_response(body).unwrap();
389        let meta = builder.build();
390        assert_eq!(meta.code(), Some("InvalidGreeting"));
391        assert_eq!(meta.message(), Some("Hi"));
392        assert_eq!(meta.extra("request_id"), Some("req-1"));
393
394        // The deserializer should be positioned inside <Error> and able to read members
395        let mut greeting = String::new();
396        deser
397            .read_struct(&OP_SCHEMA, &mut |member, d| {
398                if member.member_name() == Some("name") {
399                    greeting = d.read_string(member)?;
400                }
401                Ok(())
402            })
403            .unwrap();
404        // "Greeting" doesn't match "name" member, so greeting stays empty
405        // But Code/Message/Type are skipped as unknown — this validates no panic
406    }
407
408    #[test]
409    fn deserialize_error_unwrapped() {
410        let protocol = AwsRestXmlProtocol::new().with_no_error_wrapping(true);
411        let body =
412            b"<Error><Code>NotFound</Code><Message>Gone</Message><Detail>extra</Detail></Error>";
413
414        let (builder, _deser) = protocol.deserialize_error_response(body).unwrap();
415        let meta = builder.build();
416        assert_eq!(meta.code(), Some("NotFound"));
417        assert_eq!(meta.message(), Some("Gone"));
418    }
419
420    #[test]
421    fn deserialize_error_wrapped_ignores_literal_error_in_cdata() {
422        // Regression: the previous implementation used
423        // `body_str.find("<Error>")` to locate the envelope's `<Error>`
424        // sub-element, which would also match the literal bytes of
425        // `<Error>` inside an unrelated CDATA section. The new
426        // structural walk uses the XML parser, which treats CDATA as
427        // opaque text and only matches a real `<Error>` element.
428        //
429        // The body below has the literal bytes `<Error><Code>FAKE</Code></Error>`
430        // INSIDE a CDATA section before the real envelope's `<Error>`.
431        // The substring-search code would slice from the CDATA's
432        // `<Error>`, producing a fragment whose `Code` reads `FAKE`.
433        // The structural walk slices from the real `<Error>` and the
434        // fragment's `Code` reads `RealCode`.
435        let protocol = AwsRestXmlProtocol::new();
436        let body = b"<ErrorResponse>\
437            <Description><![CDATA[<Error><Code>FAKE</Code></Error>]]></Description>\
438            <Error><Code>RealCode</Code><Message>Real message</Message></Error>\
439            <RequestId>req-1</RequestId>\
440            </ErrorResponse>";
441
442        let (builder, _deser) = protocol.deserialize_error_response(body).unwrap();
443        let meta = builder.build();
444        // The structural walk skips the CDATA content and finds the
445        // real <Error>'s <Code>.
446        assert_eq!(meta.code(), Some("RealCode"));
447        assert_eq!(meta.message(), Some("Real message"));
448        assert_eq!(meta.extra("request_id"), Some("req-1"));
449    }
450
451    // Regression tests for `find_error_element_slice` covering the cases
452    // where a naive `body.find("<Error>")` substring match would
453    // mishandle the input.
454    #[test]
455    fn find_error_element_slice_strips_wrapped_envelope() {
456        let body =
457            b"<ErrorResponse><Error><Code>X</Code></Error><RequestId>r</RequestId></ErrorResponse>";
458        let slice = find_error_element_slice(body);
459        assert_eq!(slice, b"<Error><Code>X</Code></Error>");
460    }
461
462    #[test]
463    fn find_error_element_slice_handles_xmlns_on_inner_error() {
464        // The exact case the substring-find fails on: `xmlns` directly
465        // on the inner `<Error>` start tag means `body.find("<Error>")`
466        // returns `None` and the previous codegen returned the full
467        // body, breaking downstream error-code lookup.
468        let body = br#"<ErrorResponse><Error xmlns="http://example.com/"><Code>X</Code></Error></ErrorResponse>"#;
469        let slice = find_error_element_slice(body);
470        assert_eq!(
471            slice,
472            br#"<Error xmlns="http://example.com/"><Code>X</Code></Error>"#
473        );
474    }
475
476    #[test]
477    fn find_error_element_slice_returns_body_when_root_is_error() {
478        // `noErrorWrapping` mode: the root element IS `<Error>`. The
479        // slice should be the full body, since the root's tags are
480        // already at the boundaries.
481        let body = b"<Error><Code>X</Code><Message>m</Message></Error>";
482        let slice = find_error_element_slice(body);
483        assert_eq!(slice, body);
484    }
485
486    #[test]
487    fn find_error_element_slice_falls_back_for_missing_error() {
488        // Defensive: if the body doesn't contain an `<Error>` element
489        // anywhere, return the body unchanged so downstream parsing
490        // produces a malformed/unhandled error rather than panicking.
491        let body = b"<SomethingElse><Code>X</Code></SomethingElse>";
492        let slice = find_error_element_slice(body);
493        assert_eq!(slice, body);
494    }
495
496    #[test]
497    fn find_error_element_slice_falls_back_for_invalid_xml() {
498        // Defensive: malformed XML returns the body unchanged.
499        let body = b"not xml at all";
500        let slice = find_error_element_slice(body);
501        assert_eq!(slice, body);
502    }
503
504    #[test]
505    fn find_error_element_slice_falls_back_for_invalid_utf8() {
506        // Defensive: non-UTF-8 bytes return the body unchanged. The
507        // `Document::try_from(&[u8])` path validates UTF-8.
508        let body = &[0xFFu8, 0xFE, 0xFD][..];
509        let slice = find_error_element_slice(body);
510        assert_eq!(slice, body);
511    }
512}