Skip to main content

aws_smithy_http_server/schema/protocol/
rest_xml.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use aws_smithy_runtime_api::http::Headers;
7use aws_smithy_schema::codec::Codec;
8use aws_smithy_schema::serde::{SerdeError, SerializableStruct, ShapeDeserializer};
9use aws_smithy_schema::{shape_id, Schema, ShapeId};
10use aws_smithy_types::{BigDecimal, BigInteger, Blob, DateTime, Document};
11
12use crate::body::BoxBody;
13use crate::protocol::rest_xml::rejection::RequestRejection;
14use crate::protocol::rest_xml::runtime_error::RuntimeError;
15use crate::response::{IntoResponse, Response};
16use crate::schema::{DeserializeError, HttpModeledError};
17
18use super::request::payload_member;
19use super::response::{log_serialize_failure, stamp_error_extension};
20use super::rest::RestPolicy;
21use super::{BodyDirective, EventStreamFraming, MetadataRoutedProtocol, ServerProtocol};
22
23/// Stateful schema-driven restXml protocol implementation.
24///
25/// Wrapped collections read only their modeled item and entry names. Requests accept
26/// `application/xml`, and parsing checks the modeled root while retaining the generated serde
27/// parser's recovery from malformed XML.
28#[derive(Debug)]
29pub struct RestXml {
30    pub(crate) inner: crate::schema::protocol::rest::RestProtocol<aws_smithy_xml::codec::XmlCodec>,
31    /// Reads the `<Error>` element of received event-stream exception frames, whose root name is
32    /// not the error structure's.
33    exception_codec: aws_smithy_xml::codec::XmlCodec,
34    /// `@restXml(noErrorWrapping: true)`: exception frames are rooted at `<Error>`.
35    no_error_wrapping: bool,
36}
37
38impl RestXml {
39    fn new(
40        strict_collection_element_names: bool,
41        validate_document: bool,
42        accept_text_xml: bool,
43        no_error_wrapping: bool,
44    ) -> Self {
45        let codec = |validate_root_name: bool| {
46            aws_smithy_xml::codec::XmlCodec::new(
47                aws_smithy_xml::codec::XmlCodecSettings::builder()
48                    .error_root_name("Error")
49                    .validate_root_name(validate_root_name)
50                    .validate_document(validate_document)
51                    .strict_collection_element_names(strict_collection_element_names)
52                    .build(),
53            )
54        };
55        Self {
56            exception_codec: codec(false),
57            no_error_wrapping,
58            inner: crate::schema::protocol::rest::RestProtocol::new(
59                codec(true),
60                RestPolicy {
61                    request_content_type_aliases: if accept_text_xml {
62                        REQUEST_CONTENT_TYPE_ALIASES
63                    } else {
64                        &[]
65                    },
66                    ..POLICY
67                },
68            ),
69        }
70    }
71}
72
73impl Default for RestXml {
74    fn default() -> Self {
75        Self::new(true, false, false, false)
76    }
77}
78
79static PROTOCOL_ID: ShapeId<'static> = shape_id!("aws.protocols", "restXml");
80const CONTENT_TYPE: &str = "application/xml";
81
82/// Media types a request may use for an XML body besides [`CONTENT_TYPE`].
83const REQUEST_CONTENT_TYPE_ALIASES: &[&str] = &["text/xml"];
84
85/// restXml labels a response only when the output schema binds something to the body, gives an
86/// untyped blob payload `application/octet-stream`, and sends an empty body for an output with
87/// no body members whether or not the user modeled it.
88pub(crate) const POLICY: RestPolicy = RestPolicy {
89    payload_deserializer: Some(deserialize_payload),
90    codec_content_type: CONTENT_TYPE,
91    request_content_type_aliases: &[],
92    default_response_content_type: None,
93    untyped_blob_payload_content_type: Some("application/octet-stream"),
94    empty_document: false,
95};
96
97fn deserialize_payload(
98    member: &Schema<'_>,
99    deserializer: &mut dyn ShapeDeserializer,
100    consumer: &mut dyn FnMut(&Schema<'_>, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
101) -> Result<(), SerdeError> {
102    let Some(metadata) = crate::schema::traits::XmlMetadata::from_schema(member) else {
103        return consumer(member, deserializer);
104    };
105    consumer(
106        member,
107        &mut StructuredPayloadDeserializer {
108            inner: deserializer,
109            root: metadata.payload_schema(),
110        },
111    )
112}
113
114impl MetadataRoutedProtocol for RestXml {
115    fn from_build_context(
116        ctx: &crate::schema::ProtocolBuildContext<'_>,
117    ) -> Result<Self, crate::schema::routing::RouterBuildError> {
118        let mut protocol = Self::new(
119            crate::schema::settings::get::<bool>(ctx.settings, "strictCollectionElementNames")?.unwrap_or(true),
120            crate::schema::settings::get::<bool>(ctx.settings, "validateDocument")?.unwrap_or(false),
121            crate::schema::settings::get::<bool>(ctx.settings, "acceptTextXml")?.unwrap_or(false),
122            // A model fact, recorded by codegen on the service schema.
123            crate::schema::traits::RestXmlTrait::from_service(ctx.service)
124                .is_some_and(crate::schema::traits::RestXmlTrait::no_error_wrapping),
125        );
126        protocol
127            .inner
128            .prepare_response_plans(ctx.service)
129            .map_err(|err| crate::schema::routing::RouterBuildError::Configuration(err.to_string()))?;
130        Ok(protocol)
131    }
132
133    fn build_router(
134        &self,
135        ctx: crate::schema::routing::RouterBuildContext<'_>,
136    ) -> Result<
137        impl crate::schema::routing::MetadataProtocolRouter + 'static + use<>,
138        crate::schema::routing::RouterBuildError,
139    > {
140        crate::schema::routing::rest_router(&ctx, CONTENT_TYPE, self.inner.policy().request_content_type_aliases)
141    }
142
143    fn event_stream_framing(&self) -> Option<EventStreamFraming<'_>> {
144        Some(
145            EventStreamFraming::new(self.inner.codec(), CONTENT_TYPE)
146                .unset_initial_response_payload(b"")
147                .exception_http_bindings(true)
148                .xml_exceptions(&self.exception_codec, !self.no_error_wrapping),
149        )
150    }
151}
152
153impl ServerProtocol for RestXml {
154    fn protocol_id(&self) -> &'static ShapeId<'static> {
155        &PROTOCOL_ID
156    }
157
158    fn validate_request_headers(
159        &self,
160        operation: &crate::schema::OperationSchema<'_>,
161        headers: &Headers,
162    ) -> Result<(), DeserializeError> {
163        self.inner.check_accept(operation.output(), headers)
164    }
165
166    fn request_body_requirement(&self, operation: &crate::schema::OperationSchema<'_>) -> BodyDirective {
167        if self.inner.reads_request_body(operation.input()) {
168            BodyDirective::Collect
169        } else {
170            BodyDirective::Skip
171        }
172    }
173
174    fn deserialize_request<'a>(
175        &'a self,
176        input: &Schema<'_>,
177        request: &'a aws_smithy_runtime_api::http::Request<bytes::Bytes>,
178    ) -> Result<Box<dyn ShapeDeserializer + 'a>, DeserializeError> {
179        self.inner.deserialize_request(input, request)
180    }
181
182    fn serialize_response(&self, output: &Schema<'_>, value: &dyn SerializableStruct) -> Response {
183        self.inner
184            .serialize_response_with_codec(&ResponseXmlCodec::new(self.inner.codec(), output), output, value)
185            .unwrap_or_else(serialization_failure)
186    }
187
188    fn serialize_streaming_response(
189        &self,
190        output: &Schema<'_>,
191        value: &dyn SerializableStruct,
192        _streaming_member: &Schema<'_>,
193        body: Option<BoxBody>,
194    ) -> Response {
195        self.inner
196            .serialize_streaming_response(output, value, body.unwrap_or_else(crate::body::empty))
197            .unwrap_or_else(serialization_failure)
198    }
199
200    fn serialize_error(&self, error: &dyn HttpModeledError) -> Response {
201        // restXml carries no discriminator: the error structure is the body.
202        let schema = error.schema();
203        // Generated serde can omit this header, depending on the operation's success output.
204        let content_type = match error.operation_schema() {
205            Some(operation) => self.inner.response_content_type(operation.output()),
206            None => Some(CONTENT_TYPE),
207        };
208        self.inner
209            .serialize_modeled_error(
210                &ResponseXmlCodec::for_error(self.inner.codec()),
211                schema,
212                error,
213                error.status_code(),
214                content_type,
215            )
216            .map(|response| stamp_error_extension(response, schema.shape_id().shape_name()))
217            .unwrap_or_else(serialization_failure)
218    }
219
220    fn serialize_routing_error(&self, err: &crate::schema::routing::RoutingError) -> Response {
221        use crate::protocol::rest::router::Error;
222        use crate::schema::routing::RoutingErrorKind;
223        if err.kind() == RoutingErrorKind::MissingContentType {
224            return self.serialize_rejection(
225                crate::rejection::MissingContentTypeReason::UnexpectedMimeType {
226                    expected_mime: err.expected_content_type().and_then(|expected| expected.parse().ok()),
227                    found_mime: None,
228                }
229                .into(),
230            );
231        }
232        let error = match err.kind() {
233            RoutingErrorKind::MethodNotAllowed => Error::MethodNotAllowed,
234            _ => Error::NotFound,
235        };
236        IntoResponse::<RestXml>::into_response(error)
237    }
238
239    fn serialize_runtime_error(&self, error: crate::runtime_error::RuntimeError) -> Response {
240        IntoResponse::<RestXml>::into_response(RuntimeError::from(error))
241    }
242
243    /// restXml keeps 415 for `Content-Type` failures, but its `From<RequestRejection>` has no
244    /// `NotAcceptable` arm, so an `Accept` mismatch falls through to a 400 `Serialization`, NOT
245    /// a 406. And its `RuntimeError` response drops the validation body entirely: every runtime
246    /// error, constraint violations included, answers with the literal `{}` body, so a constraint
247    /// violation must NOT serialize the modeled error here. Both are the protocol's wire contract.
248    fn serialize_rejection(&self, err: DeserializeError) -> Response {
249        match err {
250            DeserializeError::InitialRequestFrame(reason) => {
251                IntoResponse::<RestXml>::into_response(RuntimeError::Validation(reason))
252            }
253            DeserializeError::Serde(err) => {
254                IntoResponse::<RestXml>::into_response(RuntimeError::Serialization(crate::Error::new(err)))
255            }
256            DeserializeError::BodyCollection(err) => {
257                IntoResponse::<RestXml>::into_response(RuntimeError::Serialization(crate::Error::new(err)))
258            }
259            DeserializeError::UnsupportedMediaType(reason) => IntoResponse::<RestXml>::into_response(
260                RuntimeError::from(RequestRejection::MissingContentType(*reason)),
261            ),
262            DeserializeError::NotAcceptable => {
263                IntoResponse::<RestXml>::into_response(RuntimeError::from(RequestRejection::NotAcceptable))
264            }
265            // The smuggled reason string never reaches the wire; generated serde renders `{}` regardless.
266            DeserializeError::ConstraintViolation(_) => {
267                IntoResponse::<RestXml>::into_response(RuntimeError::Validation(String::new()))
268            }
269            DeserializeError::InternalFailure(err) => {
270                IntoResponse::<RestXml>::into_response(RuntimeError::InternalFailure(err))
271            }
272        }
273    }
274}
275
276/// Configures the XML document root for this response's structured payload.
277/// The target schema still supplies its namespace and child metadata.
278struct ResponseXmlCodec<'a> {
279    inner: &'a aws_smithy_xml::codec::XmlCodec,
280    payload_member: Option<&'a Schema<'a>>,
281}
282
283impl<'a> ResponseXmlCodec<'a> {
284    fn new(inner: &'a aws_smithy_xml::codec::XmlCodec, output: &'a Schema<'a>) -> Self {
285        Self {
286            inner,
287            payload_member: payload_member(output),
288        }
289    }
290
291    /// Modeled errors are documents rooted at `Error`: like generated serde servers, they drop an
292    /// `@httpPayload` member, so its `@xmlName` never names the root.
293    fn for_error(inner: &'a aws_smithy_xml::codec::XmlCodec) -> Self {
294        Self {
295            inner,
296            payload_member: None,
297        }
298    }
299}
300
301impl Codec for ResponseXmlCodec<'_> {
302    type Serializer = aws_smithy_xml::codec::XmlSerializer;
303    type Deserializer<'a> = aws_smithy_xml::codec::XmlDeserializer<'a>;
304
305    fn create_serializer(&self) -> Self::Serializer {
306        let mut serializer = self.inner.create_serializer();
307        // Generated serde payload naming gives the member's @xmlName precedence over the target.
308        // Without an override, the serializer resolves the target's name as usual.
309        if let Some(name) = self.payload_member.and_then(|member| member.xml_name()) {
310            serializer.set_next_root_xml_name(name.value().to_owned());
311        }
312        serializer
313    }
314
315    fn create_deserializer<'a>(&self, input: &'a [u8]) -> Self::Deserializer<'a> {
316        self.inner.create_deserializer(input)
317    }
318}
319
320fn serialization_failure(err: SerdeError) -> Response {
321    log_serialize_failure(&err);
322    IntoResponse::<RestXml>::into_response(RuntimeError::Serialization(crate::Error::new(err)))
323}
324
325macro_rules! unsupported_reads {
326    ($why:literal; $($method:ident -> $ret:ty),+ $(,)?) => {
327        $(
328            fn $method(&mut self, _schema: &Schema<'_>) -> Result<$ret, SerdeError> {
329                Err(SerdeError::unsupported($why))
330            }
331        )+
332    };
333}
334
335/// Wraps a codec deserializer to preserve a structured `@httpPayload` member's
336/// XML name when generated deserialization switches to the target shape's schema.
337/// Only the document root is aliased; child schemas and the underlying codec
338/// are passed directly to the consumer.
339///
340/// For example, if the payload member has `@xmlName("PayloadRoot")` and its target
341/// structure has `@xmlName("TargetRoot")`, the request body uses the member's name:
342///
343/// ```xml
344/// <PayloadRoot>
345///     <Child>hello</Child>
346/// </PayloadRoot>
347/// ```
348///
349/// This wrapper makes the codec expect `PayloadRoot` when the generated walker
350/// passes the target structure's schema. The child's XML name remains `Child`.
351struct StructuredPayloadDeserializer<'a> {
352    inner: &'a mut dyn ShapeDeserializer,
353    root: &'static Schema<'static>,
354}
355
356impl ShapeDeserializer for StructuredPayloadDeserializer<'_> {
357    fn read_struct(
358        &mut self,
359        _schema: &Schema<'_>,
360        consumer: &mut dyn FnMut(&Schema<'_>, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
361    ) -> Result<(), SerdeError> {
362        self.inner.read_struct(self.root, consumer)
363    }
364
365    fn read_list(
366        &mut self,
367        schema: &Schema<'_>,
368        consumer: &mut dyn FnMut(&mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
369    ) -> Result<(), SerdeError> {
370        self.inner.read_list(schema, consumer)
371    }
372
373    fn read_map(
374        &mut self,
375        schema: &Schema<'_>,
376        consumer: &mut dyn FnMut(String, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
377    ) -> Result<(), SerdeError> {
378        self.inner.read_map(schema, consumer)
379    }
380
381    unsupported_reads! {
382        "expected a structured payload";
383        read_boolean -> bool,
384        read_byte -> i8,
385        read_short -> i16,
386        read_integer -> i32,
387        read_long -> i64,
388        read_float -> f32,
389        read_double -> f64,
390        read_big_integer -> BigInteger,
391        read_big_decimal -> BigDecimal,
392        read_blob -> Blob,
393        read_timestamp -> DateTime,
394        read_string -> String,
395    }
396    fn read_document(&mut self, schema: &Schema<'_>) -> Result<Document, SerdeError> {
397        self.inner.read_document(schema)
398    }
399    fn is_null(&self) -> bool {
400        self.inner.is_null()
401    }
402    fn container_size(&self) -> Option<usize> {
403        self.inner.container_size()
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410    use aws_smithy_schema::ShapeType;
411
412    #[test]
413    fn structured_payload_preserves_target_metadata_when_aliasing_xml_root() {
414        static TRAITS: std::sync::LazyLock<aws_smithy_schema::TraitMap> = std::sync::LazyLock::new(|| {
415            let mut traits = aws_smithy_schema::TraitMap::new();
416            traits.insert(Box::new(aws_smithy_schema::StringTrait::new(
417                aws_smithy_schema::shape_id!("test", "custom"),
418                "preserved",
419            )));
420            traits
421        });
422        static CHILD: Schema = Schema::new_member(
423            aws_smithy_schema::shape_id!("test", "Target", "value"),
424            ShapeType::String,
425            "value",
426            0,
427        )
428        .with_xml_name("Child");
429        static TARGET: Schema = Schema::new_struct(
430            aws_smithy_schema::shape_id!("test", "Target"),
431            ShapeType::Structure,
432            &[&CHILD],
433        )
434        .with_xml_name("TargetRoot")
435        .with_original_name("OriginalTarget")
436        .with_xml_namespace("urn:test:target", Some("t"))
437        .with_sensitive()
438        .with_xml_unwrapped_output()
439        .with_traits(&TRAITS);
440
441        /// Test codec stand-in that checks root aliasing preserves the target's
442        /// other metadata and passes the original child schema to the consumer.
443        struct MetadataCheckingDeserializer;
444        impl ShapeDeserializer for MetadataCheckingDeserializer {
445            fn read_struct(
446                &mut self,
447                schema: &Schema<'_>,
448                consumer: &mut dyn FnMut(&Schema<'_>, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
449            ) -> Result<(), SerdeError> {
450                assert_eq!(schema.shape_id(), TARGET.shape_id());
451                assert_eq!(schema.shape_type(), TARGET.shape_type());
452                assert_eq!(schema.xml_name().unwrap().value(), "PayloadRoot");
453                assert_eq!(schema.original_name(), Some("OriginalTarget"));
454                let namespace = schema.xml_namespace().expect("target namespace preserved");
455                assert_eq!(namespace.uri(), "urn:test:target");
456                assert_eq!(namespace.prefix(), Some("t"));
457                assert!(schema.sensitive().is_some());
458                assert!(schema.xml_unwrapped_output());
459                assert!(std::ptr::eq(schema.traits().unwrap(), TARGET.traits().unwrap()));
460                assert_eq!(schema.members().len(), 1);
461                assert!(std::ptr::eq(schema.members()[0], &CHILD));
462                consumer(schema.members()[0], self)
463            }
464
465            fn read_list(
466                &mut self,
467                _: &Schema<'_>,
468                _: &mut dyn FnMut(&mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
469            ) -> Result<(), SerdeError> {
470                unreachable!()
471            }
472
473            fn read_map(
474                &mut self,
475                _: &Schema<'_>,
476                _: &mut dyn FnMut(String, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
477            ) -> Result<(), SerdeError> {
478                unreachable!()
479            }
480
481            unsupported_reads! {
482                "metadata checking only";
483                read_boolean -> bool,
484                read_byte -> i8,
485                read_short -> i16,
486                read_integer -> i32,
487                read_long -> i64,
488                read_float -> f32,
489                read_double -> f64,
490                read_big_integer -> BigInteger,
491                read_big_decimal -> BigDecimal,
492                read_blob -> Blob,
493                read_timestamp -> DateTime,
494                read_document -> Document,
495            }
496
497            fn read_string(&mut self, schema: &Schema<'_>) -> Result<String, SerdeError> {
498                assert!(std::ptr::eq(schema, &CHILD));
499                Ok("decoded".into())
500            }
501
502            fn is_null(&self) -> bool {
503                false
504            }
505
506            fn container_size(&self) -> Option<usize> {
507                None
508            }
509        }
510
511        static PAYLOAD: Schema = Schema::new_struct(
512            aws_smithy_schema::shape_id!("test", "Target"),
513            ShapeType::Structure,
514            &[&CHILD],
515        )
516        .with_xml_name("PayloadRoot")
517        .with_original_name("OriginalTarget")
518        .with_xml_namespace("urn:test:target", Some("t"))
519        .with_sensitive()
520        .with_xml_unwrapped_output()
521        .with_traits(&TRAITS);
522        static MEMBER_TRAITS: std::sync::LazyLock<aws_smithy_schema::TraitMap> = std::sync::LazyLock::new(|| {
523            let mut traits = aws_smithy_schema::TraitMap::new();
524            traits.insert(Box::new(crate::schema::traits::XmlMetadata::new(&PAYLOAD)));
525            traits
526        });
527        static MEMBER_WITH_METADATA: Schema = Schema::new_member(
528            aws_smithy_schema::shape_id!("test", "Input", "data"),
529            ShapeType::Structure,
530            "data",
531            0,
532        )
533        .with_xml_name("PayloadRoot")
534        .with_traits(&MEMBER_TRAITS);
535
536        let member = &MEMBER_WITH_METADATA;
537        let mut inner = MetadataCheckingDeserializer;
538        let mut value = None;
539        deserialize_payload(member, &mut inner, &mut |actual_member, payload| {
540            assert!(std::ptr::eq(actual_member, member));
541            payload.read_struct(&TARGET, &mut |child, deser| {
542                value = Some(deser.read_string(child)?);
543                Ok(())
544            })
545        })
546        .unwrap();
547        assert_eq!(value.as_deref(), Some("decoded"));
548        assert_eq!(TARGET.xml_name().unwrap().value(), "TargetRoot");
549    }
550}