Skip to main content

aws_smithy_http_server/schema/protocol/
aws_json.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! awsJson1.0 and awsJson1.1 share a codec, a rejection type and a runtime error; they differ in
7//! the content type and in whether the `__type` discriminator is the full shape ID or the name.
8
9use aws_smithy_runtime_api::http::Headers;
10use aws_smithy_schema::serde::{SerdeError, SerializableStruct, ShapeDeserializer};
11use aws_smithy_schema::{shape_id, Schema, ShapeId};
12
13use crate::body::BoxBody;
14use crate::protocol::aws_json::rejection::RequestRejection;
15use crate::protocol::aws_json::runtime_error::RuntimeError;
16use crate::response::{IntoResponse, Response};
17use crate::schema::response_bindings::{serialize_response_parts, ResponseValueKind};
18use crate::schema::{DeserializeError, HttpModeledError};
19
20use super::discriminator::{BodyDiscriminator, TypeValue};
21use super::response::{
22    assemble_response, assemble_streaming_response, log_serialize_failure, resolve_status,
23    serialize_modeled_error_response, stamp_error_extension, stamp_validation_extension, ResponseBindings,
24};
25use super::{BodyDirective, EventStreamFraming, MetadataRoutedProtocol, ServerProtocol};
26
27/// Stateful schema-driven AWS JSON 1.0 protocol implementation.
28#[derive(Debug)]
29/// Defaults to skipping escape validation in discarded strings.
30/// Set `customizationConfig.protocols` for this protocol to `{"validateSkippedValues":true}`
31/// to validate escape syntax without decoding Unicode escapes.
32/// UTF-8, unescaped control characters, and structural syntax remain checked in either mode.
33///
34/// `responseBindings` defaults to `"historical"`, which keeps what smithy-rs awsJson servers have
35/// always sent: the operation's `@http(code)`, `@httpError` statuses and `@httpHeader` members
36/// as response headers (each also stays in the document). Set it to `"smithy-specs"` in this
37/// protocol's settings for the AWS JSON specification instead: always 200 (errors use the
38/// `@error` class status) with every member in the document and no REST headers.
39pub struct AwsJson1_0 {
40    pub(crate) inner: crate::schema::protocol::rpc::RpcProtocol<aws_smithy_json::codec::JsonCodec>,
41    response_bindings: ResponseBindingsMode,
42}
43
44impl Default for AwsJson1_0 {
45    fn default() -> Self {
46        Self::new(false, ResponseBindingsMode::Historical)
47    }
48}
49
50impl AwsJson1_0 {
51    fn new(validate_skipped_values: bool, response_bindings: ResponseBindingsMode) -> Self {
52        Self {
53            response_bindings,
54            inner: crate::schema::protocol::rpc::RpcProtocol::new(
55                schema_codec(validate_skipped_values),
56                "application/x-amz-json-1.0",
57                Some("application/x-amz-json-1.0"),
58                crate::schema::protocol::rpc::RpcAccept::Always,
59                crate::schema::protocol::rpc::RpcStreaming::CodecContentType,
60            ),
61        }
62    }
63}
64
65/// Stateful schema-driven AWS JSON 1.1 protocol implementation.
66#[derive(Debug)]
67/// Defaults to skipping escape validation in discarded strings.
68/// Set `customizationConfig.protocols` for this protocol to `{"validateSkippedValues":true}`
69/// to validate escape syntax without decoding Unicode escapes.
70/// UTF-8, unescaped control characters, and structural syntax remain checked in either mode.
71///
72/// `responseBindings` defaults to `"historical"`, which keeps what smithy-rs awsJson servers have
73/// always sent: the operation's `@http(code)`, `@httpError` statuses and `@httpHeader` members
74/// as response headers (each also stays in the document). Set it to `"smithy-specs"` in this
75/// protocol's settings for the AWS JSON specification instead: always 200 (errors use the
76/// `@error` class status) with every member in the document and no REST headers.
77pub struct AwsJson1_1 {
78    pub(crate) inner: crate::schema::protocol::rpc::RpcProtocol<aws_smithy_json::codec::JsonCodec>,
79    response_bindings: ResponseBindingsMode,
80}
81
82impl Default for AwsJson1_1 {
83    fn default() -> Self {
84        Self::new(false, ResponseBindingsMode::Historical)
85    }
86}
87
88impl AwsJson1_1 {
89    fn new(validate_skipped_values: bool, response_bindings: ResponseBindingsMode) -> Self {
90        Self {
91            response_bindings,
92            inner: crate::schema::protocol::rpc::RpcProtocol::new(
93                schema_codec(validate_skipped_values),
94                "application/x-amz-json-1.1",
95                Some("application/x-amz-json-1.1"),
96                crate::schema::protocol::rpc::RpcAccept::Always,
97                crate::schema::protocol::rpc::RpcStreaming::CodecContentType,
98            ),
99        }
100    }
101}
102
103/// How REST response bindings are applied to AWS JSON responses, from the `responseBindings` setting.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105enum ResponseBindingsMode {
106    /// `"historical"` (the default): what smithy-rs awsJson servers have always sent.
107    Historical,
108    /// `"smithy-specs"`: the AWS JSON specification behavior.
109    SmithySpecs,
110}
111
112/// Reads the `responseBindings` setting: `"historical"` (the default) or `"smithy-specs"`.
113fn response_bindings_mode(
114    settings: Option<&aws_smithy_types::Document>,
115) -> Result<ResponseBindingsMode, crate::schema::routing::RouterBuildError> {
116    match crate::schema::settings::get::<&str>(settings, "responseBindings")? {
117        None | Some("historical") => Ok(ResponseBindingsMode::Historical),
118        Some("smithy-specs") => Ok(ResponseBindingsMode::SmithySpecs),
119        Some(other) => Err(crate::schema::routing::RouterBuildError::Configuration(format!(
120            "setting `responseBindings` must be \"historical\" or \"smithy-specs\", got {other:?}"
121        ))),
122    }
123}
124
125fn schema_codec(validate_skipped_values: bool) -> aws_smithy_json::codec::JsonCodec {
126    aws_smithy_json::codec::JsonCodec::new(
127        aws_smithy_json::codec::JsonCodecSettings::builder()
128            .use_json_name(false)
129            .default_timestamp_format(aws_smithy_types::date_time::Format::EpochSeconds)
130            .enforce_strictness(true)
131            .allow_leading_zeros(true)
132            .allow_trailing_decimal_point(true)
133            .allow_empty_integer_part(true)
134            .normalize_arbitrary_precision(true)
135            .validate_skipped_values(validate_skipped_values)
136            .validate_skipped_string_encoding(true)
137            .allow_integral_float_numbers(true)
138            .strict_timestamp_formats(true)
139            .dispatch_null_members(true)
140            .build(),
141    )
142}
143
144// AWS JSON uses @error classification, independently of the REST @httpError override.
145fn classification_status(schema: &Schema<'_>) -> Result<u16, SerdeError> {
146    let classification = schema
147        .traits()
148        .and_then(|traits| traits.get(&shape_id!("smithy.api", "error")))
149        .and_then(|value| value.as_any().downcast_ref::<aws_smithy_schema::StringTrait>())
150        .map(|value| value.value());
151    match classification {
152        Some("client") => Ok(400),
153        Some("server") => Ok(500),
154        _ => Err(SerdeError::custom("missing or invalid @error trait")),
155    }
156}
157
158fn serialize_error<P>(
159    codec: &aws_smithy_json::codec::JsonCodec,
160    error: &dyn HttpModeledError,
161    content_type: &'static str,
162    discriminator: BodyDiscriminator,
163    mode: ResponseBindingsMode,
164) -> Response
165where
166    RuntimeError: IntoResponse<P>,
167{
168    let schema = error.schema();
169    let status = match mode {
170        ResponseBindingsMode::Historical => error.status_code(),
171        ResponseBindingsMode::SmithySpecs => match classification_status(schema) {
172            Ok(status) => status,
173            Err(error) => return serialization_failure::<P>(error),
174        },
175    };
176    let framed = discriminator.frame(schema, error);
177    let bindings = match mode {
178        ResponseBindingsMode::Historical => ResponseBindings::LegacyRpc,
179        ResponseBindingsMode::SmithySpecs => ResponseBindings::BodyOnly,
180    };
181    serialize_modeled_error_response(codec, schema, &framed, status, bindings, content_type)
182        .map(|response| stamp_error_extension(response, schema.shape_id().shape_name()))
183        .unwrap_or_else(serialization_failure::<P>)
184}
185
186fn serialization_failure<P>(err: SerdeError) -> Response
187where
188    RuntimeError: IntoResponse<P>,
189{
190    log_serialize_failure(&err);
191    IntoResponse::<P>::into_response(RuntimeError::Serialization(crate::Error::new(err)))
192}
193
194macro_rules! aws_json_protocol {
195    ($protocol:ty, $marker:ty, $protocol_id:expr, $content_type:literal, $type_value:expr) => {
196        impl MetadataRoutedProtocol for $protocol {
197            fn from_build_context(
198                ctx: &crate::schema::ProtocolBuildContext<'_>,
199            ) -> Result<Self, crate::schema::routing::RouterBuildError> {
200                Ok(Self::new(
201                    crate::schema::settings::get::<bool>(ctx.settings, "validateSkippedValues")?.unwrap_or(false),
202                    response_bindings_mode(ctx.settings)?,
203                ))
204            }
205
206            fn build_router(
207                &self,
208                ctx: crate::schema::routing::RouterBuildContext<'_>,
209            ) -> Result<
210                impl crate::schema::routing::MetadataProtocolRouter + 'static + use<>,
211                crate::schema::routing::RouterBuildError,
212            > {
213                crate::schema::routing::aws_json_router(&ctx, $content_type)
214            }
215
216            fn event_stream_framing(&self) -> Option<EventStreamFraming<'_>> {
217                Some(
218                    EventStreamFraming::new(self.inner.codec(), "application/json")
219                        .initial_messages_in_frames(true)
220                        .exception_discriminator(BodyDiscriminator::new($type_value)),
221                )
222            }
223        }
224
225        impl ServerProtocol for $protocol {
226            fn protocol_id(&self) -> &'static ShapeId<'static> {
227                static PROTOCOL_ID: ShapeId<'static> = $protocol_id;
228                &PROTOCOL_ID
229            }
230
231            fn validate_request_headers(
232                &self,
233                operation: &crate::schema::OperationSchema<'_>,
234                headers: &Headers,
235            ) -> Result<(), DeserializeError> {
236                self.inner.check_accept(operation.output(), headers)
237            }
238
239            fn request_body_requirement(&self, operation: &crate::schema::OperationSchema<'_>) -> BodyDirective {
240                if self.inner.reads_request_body(operation.input()) {
241                    BodyDirective::Collect
242                } else {
243                    BodyDirective::Skip
244                }
245            }
246
247            fn deserialize_request<'a>(
248                &'a self,
249                input: &Schema<'_>,
250                request: &'a aws_smithy_runtime_api::http::Request<bytes::Bytes>,
251            ) -> Result<Box<dyn ShapeDeserializer + 'a>, DeserializeError> {
252                self.inner.deserialize_request(input, request)
253            }
254
255            fn serialize_response(&self, output: &Schema<'_>, value: &dyn SerializableStruct) -> Response {
256                if self.response_bindings == ResponseBindingsMode::Historical {
257                    return serialize_response_parts(
258                        self.inner.codec(),
259                        output,
260                        value,
261                        ResponseBindings::LegacyRpc,
262                        ResponseValueKind::OperationOutput { empty_document: true },
263                    )
264                    .and_then(|parts| {
265                        assemble_response(parts, resolve_status(None, output.http()), Some($content_type))
266                    })
267                    .unwrap_or_else(serialization_failure::<$marker>);
268                }
269                self.inner
270                    .serialize_response(output, value)
271                    .unwrap_or_else(serialization_failure::<$marker>)
272            }
273
274            fn serialize_streaming_response(
275                &self,
276                output: &Schema<'_>,
277                value: &dyn SerializableStruct,
278                _streaming_member: &Schema<'_>,
279                body: Option<BoxBody>,
280            ) -> Response {
281                if self.response_bindings == ResponseBindingsMode::Historical {
282                    return serialize_response_parts(
283                        self.inner.codec(),
284                        output,
285                        value,
286                        ResponseBindings::LegacyRpc,
287                        ResponseValueKind::StreamingOutput,
288                    )
289                    .and_then(|parts| {
290                        assemble_streaming_response(
291                            parts,
292                            resolve_status(None, output.http()),
293                            Some($content_type),
294                            body.unwrap_or_else(crate::body::empty),
295                        )
296                    })
297                    .unwrap_or_else(serialization_failure::<$marker>);
298                }
299                self.inner
300                    .serialize_streaming_response(output, value, body.unwrap_or_else(crate::body::empty))
301                    .unwrap_or_else(serialization_failure::<$marker>)
302            }
303
304            fn serialize_error(&self, error: &dyn HttpModeledError) -> Response {
305                serialize_error::<$marker>(
306                    self.inner.codec(),
307                    error,
308                    $content_type,
309                    BodyDiscriminator::new($type_value),
310                    self.response_bindings,
311                )
312            }
313
314            fn serialize_routing_error(&self, err: &crate::schema::routing::RoutingError) -> Response {
315                use crate::protocol::aws_json::router::Error;
316                use crate::schema::routing::RoutingErrorKind;
317                let error = match err.kind() {
318                    RoutingErrorKind::MethodNotAllowed => Error::MethodNotAllowed,
319                    _ => Error::NotFound,
320                };
321                IntoResponse::<$marker>::into_response(error)
322            }
323
324            fn serialize_runtime_error(&self, error: crate::runtime_error::RuntimeError) -> Response {
325                IntoResponse::<$marker>::into_response(RuntimeError::from(error))
326            }
327
328            /// awsJson's `From<RequestRejection>` collapses every transport failure, `Accept`
329            /// and `Content-Type` mismatches included, into a 400 `Serialization`; routing
330            /// through that same `From` preserves the collapse. A constraint violation answers
331            /// with the modeled validation error.
332            fn serialize_rejection(&self, err: DeserializeError) -> Response {
333                match err {
334                    DeserializeError::InitialRequestFrame(reason) => {
335                        IntoResponse::<$marker>::into_response(RuntimeError::Validation(reason))
336                    }
337                    DeserializeError::Serde(err) => {
338                        IntoResponse::<$marker>::into_response(RuntimeError::Serialization(crate::Error::new(err)))
339                    }
340                    DeserializeError::BodyCollection(err) => {
341                        IntoResponse::<$marker>::into_response(RuntimeError::Serialization(crate::Error::new(err)))
342                    }
343                    DeserializeError::UnsupportedMediaType(reason) => IntoResponse::<$marker>::into_response(
344                        RuntimeError::from(RequestRejection::MissingContentType(*reason)),
345                    ),
346                    DeserializeError::NotAcceptable => {
347                        IntoResponse::<$marker>::into_response(RuntimeError::from(RequestRejection::NotAcceptable))
348                    }
349                    DeserializeError::ConstraintViolation(err) => {
350                        stamp_validation_extension(self.serialize_error(&*err))
351                    }
352                    DeserializeError::InternalFailure(err) => {
353                        IntoResponse::<$marker>::into_response(RuntimeError::InternalFailure(err))
354                    }
355                }
356            }
357        }
358    };
359}
360
361aws_json_protocol!(
362    AwsJson1_0,
363    AwsJson1_0,
364    shape_id!("aws.protocols", "awsJson1_0"),
365    "application/x-amz-json-1.0",
366    TypeValue::FullShapeId
367);
368aws_json_protocol!(
369    AwsJson1_1,
370    AwsJson1_1,
371    shape_id!("aws.protocols", "awsJson1_1"),
372    "application/x-amz-json-1.1",
373    TypeValue::ShapeName
374);