Skip to main content

aws_smithy_json/protocol/
aws_json_rpc.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! AWS JSON RPC protocol implementation (`awsJson1_0` and `awsJson1_1`).
7//!
8//! # Protocol behaviors
9//!
10//! - HTTP method: always POST, path: always `/`
11//! - `X-Amz-Target`: `{ServiceName}.{OperationName}` (required)
12//! - Does **not** use `@jsonName` trait
13//! - Default timestamp format: `epoch-seconds`
14//! - Ignores HTTP binding traits
15//!
16//! # Differences between 1.0 and 1.1
17//!
18//! - Content-Type: `application/x-amz-json-1.0` vs `application/x-amz-json-1.1`
19//! - Error `__type` serialization differs on the server side, but clients MUST
20//!   accept either format for both versions.
21
22use crate::codec::{JsonCodec, JsonCodecSettings};
23use aws_smithy_runtime_api::client::orchestrator::Metadata;
24use aws_smithy_schema::http_protocol::HttpRpcProtocol;
25use aws_smithy_schema::protocol::ServiceShapeName;
26use aws_smithy_schema::{shape_id, Schema, ShapeId};
27use aws_smithy_types::config_bag::ConfigBag;
28
29/// AWS JSON RPC protocol (`awsJson1_0` / `awsJson1_1`).
30#[derive(Debug)]
31pub struct AwsJsonRpcProtocol {
32    inner: HttpRpcProtocol<JsonCodec>,
33    /// Prefix of the `X-Amz-Target` header. `None` means "resolve from the config bag", which is
34    /// the normal case — see [`Self::with_target_prefix`].
35    target_prefix: Option<String>,
36}
37
38impl AwsJsonRpcProtocol {
39    /// Creates an AWS JSON 1.0 protocol instance.
40    ///
41    /// The `X-Amz-Target` prefix defaults to the Smithy service shape name from the config bag;
42    /// use [`Self::with_target_prefix`] to override it.
43    pub fn aws_json_1_0() -> Self {
44        Self::new(
45            shape_id!("aws.protocols", "awsJson1_0"),
46            "application/x-amz-json-1.0",
47        )
48    }
49
50    /// Creates an AWS JSON 1.1 protocol instance.
51    ///
52    /// The `X-Amz-Target` prefix defaults to the Smithy service shape name from the config bag;
53    /// use [`Self::with_target_prefix`] to override it.
54    pub fn aws_json_1_1() -> Self {
55        Self::new(
56            shape_id!("aws.protocols", "awsJson1_1"),
57            "application/x-amz-json-1.1",
58        )
59    }
60
61    fn new(protocol_id: ShapeId<'static>, content_type: &'static str) -> Self {
62        let codec = JsonCodec::new(
63            JsonCodecSettings::builder()
64                .use_json_name(false)
65                .default_timestamp_format(aws_smithy_types::date_time::Format::EpochSeconds)
66                .protocol_id(protocol_id.clone())
67                .build(),
68        );
69        Self {
70            inner: HttpRpcProtocol::new(protocol_id, codec, content_type),
71            target_prefix: None,
72        }
73    }
74
75    /// Overrides the prefix of the `X-Amz-Target` header.
76    ///
77    /// By default the prefix is the Smithy service shape name, read from the
78    /// [`ServiceShapeName`] config-bag entry that generated clients store regardless of which
79    /// protocol they were generated for. That default exists because a customer selecting this
80    /// protocol through `Config::builder().protocol(..)` has no way to know the shape name, and an
81    /// incorrect `X-Amz-Target` is unroutable.
82    ///
83    /// Override it when a service's target prefix is not its shape name.
84    ///
85    /// Note this is *not* the sdkId carried by
86    /// [`Metadata::service`](aws_smithy_runtime_api::client::orchestrator::Metadata::service) —
87    /// an sdkId may contain spaces (`"JSON RPC 10"`) and is not a valid header value here.
88    pub fn with_target_prefix(mut self, target_prefix: impl Into<String>) -> Self {
89        self.target_prefix = Some(target_prefix.into());
90        self
91    }
92
93    /// Resolves the `X-Amz-Target` prefix: an explicit override wins, otherwise the service shape
94    /// name from the config bag. `None` when neither is available, in which case there is no
95    /// correct value to send.
96    fn resolved_target_prefix<'a>(&'a self, cfg: &'a ConfigBag) -> Option<&'a str> {
97        self.target_prefix
98            .as_deref()
99            .or_else(|| cfg.load::<ServiceShapeName>().map(ServiceShapeName::as_str))
100    }
101
102    /// Configures the default Smithy namespace used to resolve relative
103    /// shape IDs in JSON `__type` discriminator fields. Forwarded to
104    /// [`JsonCodecSettings::default_namespace`] on the codec wrapped by
105    /// this protocol.
106    ///
107    /// AWS JSON 1.0 / 1.1 services typically emit relative `__type`
108    /// values (the shape name without a namespace prefix). Code-generated
109    /// clients call this method with the service shape's namespace so
110    /// that [`crate::codec::JsonDeserializer::read_discriminated_document`]
111    /// can produce a fully-qualified discriminator.
112    ///
113    /// Unlike the `X-Amz-Target` prefix, this is a response-parsing concern rather than request
114    /// shaping — nothing on the serialization path reads it, because a serializer must always
115    /// emit an absolute shape ID.
116    ///
117    /// Setting it explicitly *overrides* the default, which is the
118    /// [`ServiceShapeNamespace`](aws_smithy_schema::protocol::ServiceShapeNamespace) config-bag
119    /// entry that generated clients store regardless of which protocol they were generated for.
120    /// A caller selecting this protocol at runtime therefore gets relative `__type` resolution
121    /// without having to know the model's namespace; previously it had to be set by hand or
122    /// discriminators stayed relative, which made the type registry silently miss the shape.
123    ///
124    /// The fallback is applied per response because `JsonCodec` holds an `Arc<JsonCodecSettings>`
125    /// that `create_deserializer` clones by pointer, so there is nowhere to store the resolved
126    /// value on an immutable protocol. That costs ~46 ns per response and only when no explicit
127    /// namespace was configured. The internal `codec_with_bag_namespace` helper in
128    /// `protocol/mod.rs` carries the
129    /// measurement and the memoization option if it ever matters.
130    pub fn with_default_namespace(self, namespace: impl Into<String>) -> Self {
131        let new_settings = self
132            .inner
133            .codec()
134            .settings()
135            .to_builder()
136            .default_namespace(namespace)
137            .build();
138        let new_codec = JsonCodec::new(new_settings);
139        Self {
140            inner: self.inner.with_codec(new_codec),
141            target_prefix: self.target_prefix,
142        }
143    }
144}
145
146impl aws_smithy_schema::protocol::ClientProtocolInner for AwsJsonRpcProtocol {
147    type Request = aws_smithy_runtime_api::http::Request;
148    type Response = aws_smithy_runtime_api::http::Response;
149
150    fn protocol_id(&self) -> &ShapeId<'static> {
151        self.inner.protocol_id()
152    }
153
154    /// Serializes an awsJson1_0 / awsJson1_1 request.
155    ///
156    /// `_endpoint` is deliberately ignored: both protocols fix the request path at `/`, so the
157    /// route is a function of the protocol rather than of the operation, and a path computed by
158    /// codegen for a different protocol must not leak through when this protocol is selected at
159    /// runtime via `Config::builder().protocol(..)`. `apply_http_endpoint` merges the scheme and
160    /// authority afterwards.
161    ///
162    /// The assertion belongs here rather than in [`HttpRpcProtocol`] because only the concrete
163    /// protocol knows whether its route is constant — `RpcV2CborProtocol` legitimately passes
164    /// `HttpRpcProtocol` a computed `/service/{service}/operation/{operation}` route.
165    fn serialize_request(
166        &self,
167        input: &dyn aws_smithy_schema::serde::SerializableStruct,
168        input_schema: &Schema<'_>,
169        _endpoint: &str,
170        cfg: &ConfigBag,
171    ) -> Result<aws_smithy_runtime_api::http::Request, aws_smithy_schema::serde::SerdeError> {
172        let mut request = self
173            .inner
174            .serialize_request(input, input_schema, "/", cfg)?;
175        if let (Some(prefix), Some(metadata)) =
176            (self.resolved_target_prefix(cfg), cfg.load::<Metadata>())
177        {
178            request
179                .headers_mut()
180                .insert("X-Amz-Target", format!("{}.{}", prefix, metadata.name()));
181        }
182        Ok(request)
183    }
184
185    fn deserialize_response<'a>(
186        &self,
187        response: &'a aws_smithy_runtime_api::http::Response,
188        output_schema: &Schema<'_>,
189        cfg: &'a ConfigBag,
190    ) -> Result<
191        Box<dyn aws_smithy_schema::serde::ShapeDeserializer + 'a>,
192        aws_smithy_schema::serde::SerdeError,
193    > {
194        // When no namespace was configured explicitly, fall back to the one generated clients
195        // store in the config bag, so a protocol selected at runtime can still resolve relative
196        // `__type` discriminators. See `crate::protocol::codec_with_bag_namespace`.
197        if let Some(codec) = crate::protocol::codec_with_bag_namespace(self.inner.codec(), cfg) {
198            // Body extraction mirrors `HttpRpcProtocol::deserialize_response`, which defers to
199            // `HttpBindingProtocol::deserialize_response` for the rationale behind tolerating an
200            // unreadable (streaming) body. Kept in step with those methods.
201            let body = response.body().bytes().unwrap_or(&[]);
202            return Ok(Box::new(
203                aws_smithy_schema::codec::Codec::create_deserializer(&codec, body),
204            ));
205        }
206        self.inner
207            .deserialize_response(response, output_schema, cfg)
208    }
209
210    /// Extracts canonical error metadata from an `awsJson1_0` / `awsJson1_1`
211    /// response.
212    ///
213    /// awsJson protocols carry the error code in the `__type` (or legacy
214    /// `code`) field of the JSON body, with `X-Amzn-Errortype` taking
215    /// priority. The error message comes from `message`, `Message`, or
216    /// `errorMessage` body keys.
217    ///
218    /// Per the
219    /// [`ClientProtocolInner::parse_error_metadata`](aws_smithy_schema::protocol::ClientProtocolInner::parse_error_metadata)
220    /// contract the request id is **not** populated here — the
221    /// orchestrator's request-id pipeline attaches it separately.
222    ///
223    /// `deserialize_error_response` is **not** overridden: awsJson has no
224    /// error envelope, so the default (which forwards to
225    /// `deserialize_response` against
226    /// [`prelude::DOCUMENT`](aws_smithy_schema::prelude::DOCUMENT)) is
227    /// already correct — the body root IS the error body.
228    fn parse_error_metadata(
229        &self,
230        response: &aws_smithy_runtime_api::http::Response,
231        _cfg: &ConfigBag,
232    ) -> Result<aws_smithy_types::error::metadata::Builder, aws_smithy_schema::serde::SerdeError>
233    {
234        let body = response.body().bytes().unwrap_or(&[]);
235        crate::protocol::error::parse_error_envelope_metadata(body, response.headers())
236    }
237
238    fn payload_codec(&self) -> Option<&dyn aws_smithy_schema::codec::DynCodec> {
239        self.inner.payload_codec()
240    }
241
242    /// This protocol labels structured event-stream payloads `application/json`.
243    ///
244    /// Must stay in agreement with the code generator's
245    /// `eventStreamMessageContentType` for this protocol (`AwsJson.kt:95`), which supplies
246    /// the fallback when a protocol declares no media type.
247    fn event_stream_media_type(&self) -> Option<&str> {
248        Some("application/json")
249    }
250
251    /// Parses the same JSON error envelope as
252    /// [`ClientProtocolInner::parse_error_metadata`](aws_smithy_schema::protocol::ClientProtocolInner::parse_error_metadata), from an event-stream
253    /// frame's payload rather than an HTTP response body. An event-stream frame has
254    /// no HTTP headers, so an empty header map is passed; the discriminator comes
255    /// from the payload's `__type`.
256    fn parse_event_stream_error_metadata(
257        &self,
258        payload: &[u8],
259    ) -> Result<aws_smithy_types::error::metadata::Builder, aws_smithy_schema::serde::SerdeError>
260    {
261        crate::protocol::error::parse_error_envelope_metadata(
262            payload,
263            &aws_smithy_runtime_api::http::Headers::new(),
264        )
265    }
266
267    fn update_endpoint(
268        &self,
269        request: &mut aws_smithy_runtime_api::http::Request,
270        endpoint: &aws_smithy_types::endpoint::Endpoint,
271        cfg: &ConfigBag,
272    ) -> Result<(), aws_smithy_schema::serde::SerdeError> {
273        self.inner.update_endpoint(request, endpoint, cfg)
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use aws_smithy_schema::protocol::ClientProtocolInner;
281    use aws_smithy_schema::serde::{SerdeError, SerializableStruct, ShapeSerializer};
282    use aws_smithy_schema::ShapeType;
283    use aws_smithy_types::config_bag::Layer;
284
285    struct EmptyStruct;
286    impl SerializableStruct for EmptyStruct {
287        fn schema(&self) -> &aws_smithy_schema::Schema<'_> {
288            &TEST_SCHEMA
289        }
290
291        fn serialize_members(&self, _: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
292            Ok(())
293        }
294    }
295
296    static TEST_SCHEMA: aws_smithy_schema::Schema =
297        aws_smithy_schema::Schema::new(shape_id!("test", "Input"), ShapeType::Structure);
298
299    fn cfg_with_metadata(service: &str, operation: &str) -> ConfigBag {
300        let mut layer = Layer::new("test");
301        layer.store_put(Metadata::new(operation.to_string(), service.to_string()));
302        ConfigBag::of_layers(vec![layer])
303    }
304
305    /// The bag a schema-serde generated client actually builds: operation `Metadata` plus the
306    /// model's service shape name.
307    fn cfg_with_service_shape_name(shape_name: &'static str, operation: &str) -> ConfigBag {
308        let mut layer = Layer::new("test");
309        layer.store_put(Metadata::new(
310            operation.to_string(),
311            "Some Sdk Id".to_string(),
312        ));
313        layer.store_put(aws_smithy_schema::protocol::ServiceShapeName::new(
314            shape_name,
315        ));
316        ConfigBag::of_layers(vec![layer])
317    }
318
319    /// A customer selecting awsJson at runtime via `Config::builder().protocol(..)` has no way to
320    /// know the Smithy service shape name that belongs in `X-Amz-Target`, so the protocol defaults
321    /// it from the config-bag entry that every schema-serde client stores regardless of the
322    /// protocol it was generated for.
323    #[test]
324    fn target_prefix_defaults_to_service_shape_name_from_config_bag() {
325        let cfg = cfg_with_service_shape_name("MyService", "DoThing");
326        let request = AwsJsonRpcProtocol::aws_json_1_0()
327            .serialize_request(&EmptyStruct, &TEST_SCHEMA, "/", &cfg)
328            .unwrap();
329        assert_eq!(
330            request.headers().get("X-Amz-Target").unwrap(),
331            "MyService.DoThing"
332        );
333    }
334
335    /// An explicit prefix still wins, so a caller whose service uses a target prefix that is not
336    /// the shape name (DynamoDB's `DynamoDB_20120810`) can override it.
337    #[test]
338    fn with_target_prefix_overrides_config_bag() {
339        let cfg = cfg_with_service_shape_name("MyService", "DoThing");
340        let request = AwsJsonRpcProtocol::aws_json_1_0()
341            .with_target_prefix("DynamoDB_20120810")
342            .serialize_request(&EmptyStruct, &TEST_SCHEMA, "/", &cfg)
343            .unwrap();
344        assert_eq!(
345            request.headers().get("X-Amz-Target").unwrap(),
346            "DynamoDB_20120810.DoThing"
347        );
348    }
349
350    /// With neither an override nor a bag entry there is no correct value, so the header is
351    /// omitted rather than guessed — matching the existing behavior when `Metadata` is absent.
352    #[test]
353    fn x_amz_target_omitted_when_prefix_is_unknown() {
354        let cfg = cfg_with_metadata("Some Sdk Id", "DoThing");
355        let request = AwsJsonRpcProtocol::aws_json_1_0()
356            .serialize_request(&EmptyStruct, &TEST_SCHEMA, "/", &cfg)
357            .unwrap();
358        assert!(request.headers().get("X-Amz-Target").is_none());
359    }
360
361    #[test]
362    fn json_1_0_content_type() {
363        let request = AwsJsonRpcProtocol::aws_json_1_0()
364            .serialize_request(
365                &EmptyStruct,
366                &TEST_SCHEMA,
367                "https://example.com",
368                &ConfigBag::base(),
369            )
370            .unwrap();
371        assert_eq!(
372            request.headers().get("Content-Type").unwrap(),
373            "application/x-amz-json-1.0"
374        );
375    }
376
377    #[test]
378    fn json_1_1_content_type() {
379        let request = AwsJsonRpcProtocol::aws_json_1_1()
380            .serialize_request(
381                &EmptyStruct,
382                &TEST_SCHEMA,
383                "https://example.com",
384                &ConfigBag::base(),
385            )
386            .unwrap();
387        assert_eq!(
388            request.headers().get("Content-Type").unwrap(),
389            "application/x-amz-json-1.1"
390        );
391    }
392
393    #[test]
394    fn sets_x_amz_target() {
395        let cfg = cfg_with_metadata("MyService", "DoThing");
396        let request = AwsJsonRpcProtocol::aws_json_1_0()
397            .with_target_prefix("MyService")
398            .serialize_request(&EmptyStruct, &TEST_SCHEMA, "https://example.com", &cfg)
399            .unwrap();
400        assert_eq!(
401            request.headers().get("X-Amz-Target").unwrap(),
402            "MyService.DoThing"
403        );
404    }
405
406    /// The body deserializer handed out by `deserialize_response` must reject malformed JSON
407    /// bodies end to end, not only inside containers. These are the bodies of Smithy protocol
408    /// tests `RestJsonInvalidJsonBody_case1` and `RestJsonInvalidJsonBody_case7`.
409    #[test]
410    fn malformed_response_bodies_are_rejected() {
411        use aws_smithy_runtime_api::http::Response;
412        use aws_smithy_types::body::SdkBody;
413
414        for body in [r#"{ "int": 10 }abc"#, r#"{"int": 10,}"#] {
415            let response = Response::new(200u16.try_into().unwrap(), SdkBody::from(body));
416            let base_cfg = ConfigBag::base();
417            let mut deser = AwsJsonRpcProtocol::aws_json_1_0()
418                .deserialize_response(&response, &TEST_SCHEMA, &base_cfg)
419                .unwrap();
420            let result = deser.read_struct(&TEST_SCHEMA, &mut |_, _| Ok(()));
421            assert!(result.is_err(), "body {body:?} must be rejected");
422        }
423
424        let response = Response::new(
425            200u16.try_into().unwrap(),
426            SdkBody::from(r#"{ "int": 10 }"#),
427        );
428        let base_cfg = ConfigBag::base();
429        let mut deser = AwsJsonRpcProtocol::aws_json_1_0()
430            .deserialize_response(&response, &TEST_SCHEMA, &base_cfg)
431            .unwrap();
432        deser
433            .read_struct(&TEST_SCHEMA, &mut |_, _| Ok(()))
434            .expect("well-formed body is accepted");
435    }
436
437    /// A quoted number for a double member must be rejected through the real response path.
438    /// This is the body of Smithy protocol test `RestJsonBodyDoubleMalformedValueRejected_case0`.
439    #[test]
440    fn quoted_number_for_double_member_is_rejected() {
441        use aws_smithy_runtime_api::http::Response;
442        use aws_smithy_types::body::SdkBody;
443
444        static DOUBLE: aws_smithy_schema::Schema = aws_smithy_schema::Schema::new_member(
445            shape_id!("test", "Output", "doubleInBody"),
446            ShapeType::Double,
447            "doubleInBody",
448            0,
449        );
450        static OUTPUT: aws_smithy_schema::Schema = aws_smithy_schema::Schema::new_struct(
451            shape_id!("test", "Output"),
452            ShapeType::Structure,
453            &[&DOUBLE],
454        );
455        let read = |body: &str| {
456            let response = Response::new(200u16.try_into().unwrap(), SdkBody::from(body));
457            let base_cfg = ConfigBag::base();
458            let mut deser = AwsJsonRpcProtocol::aws_json_1_0()
459                .deserialize_response(&response, &OUTPUT, &base_cfg)
460                .unwrap();
461            let mut value = None;
462            deser
463                .read_struct(&OUTPUT, &mut |member, d| {
464                    value = Some(d.read_double(member)?);
465                    Ok(())
466                })
467                .map(|()| value)
468        };
469        assert_eq!(read(r#"{ "doubleInBody" : 123 }"#).unwrap(), Some(123.0));
470        assert!(read(r#"{ "doubleInBody" : "123" }"#).is_err());
471    }
472
473    #[test]
474    fn json_1_0_protocol_id() {
475        assert_eq!(
476            AwsJsonRpcProtocol::aws_json_1_0().protocol_id().as_str(),
477            "aws.protocols#awsJson1_0"
478        );
479    }
480
481    // ---- route ---------------------------------------------------------
482
483    /// awsJson1_0/1_1 fix the request path at `/`, so a path computed by codegen for a
484    /// *different* protocol must not win when this protocol is selected at runtime via
485    /// `Config::builder().protocol(..)`. The rpcv2Cbor route below is what an
486    /// rpcv2Cbor-generated client passes.
487    ///
488    /// This is the mirror image of https://github.com/smithy-lang/smithy-rs/issues/4801,
489    /// where the CBOR protocol failed to apply its own route.
490    #[test]
491    fn serialize_request_ignores_a_route_computed_for_another_protocol() {
492        let cfg = cfg_with_metadata("MyService", "DoThing");
493        for foreign_route in ["/service/MyService/operation/DoThing", "/stats"] {
494            let request = AwsJsonRpcProtocol::aws_json_1_0()
495                .serialize_request(&EmptyStruct, &TEST_SCHEMA, foreign_route, &cfg)
496                .unwrap();
497            assert_eq!(
498                "/",
499                request.uri(),
500                "awsJson must POST to / regardless of the path it is handed"
501            );
502            assert_eq!("POST", request.method());
503        }
504    }
505
506    /// The empty endpoint a generated awsJson client passes must keep resolving to `/`.
507    #[test]
508    fn serialize_request_defaults_to_slash() {
509        let cfg = cfg_with_metadata("MyService", "DoThing");
510        let request = AwsJsonRpcProtocol::aws_json_1_0()
511            .serialize_request(&EmptyStruct, &TEST_SCHEMA, "", &cfg)
512            .unwrap();
513        assert_eq!("/", request.uri());
514    }
515
516    #[test]
517    fn json_1_1_protocol_id() {
518        assert_eq!(
519            AwsJsonRpcProtocol::aws_json_1_1().protocol_id().as_str(),
520            "aws.protocols#awsJson1_1"
521        );
522    }
523
524    // ---- parse_error_metadata overrides --------------------------------
525
526    use aws_smithy_runtime_api::http::{Response, StatusCode};
527    use aws_smithy_types::body::SdkBody;
528
529    fn http_response(headers: &[(&str, &str)], body: &str) -> Response {
530        let mut response = Response::new(StatusCode::try_from(400).unwrap(), SdkBody::from(body));
531        for (name, value) in headers {
532            response
533                .headers_mut()
534                .insert(name.to_string(), value.to_string());
535        }
536        response
537    }
538
539    #[test]
540    fn parse_error_metadata_extracts_code_and_message_from_body() {
541        let proto = AwsJsonRpcProtocol::aws_json_1_0();
542        let response = http_response(&[], r#"{"__type":"InvalidGreeting","message":"hi"}"#);
543        let cfg = ConfigBag::base();
544        let meta = proto.parse_error_metadata(&response, &cfg).unwrap().build();
545        assert_eq!(meta.code(), Some("InvalidGreeting"));
546        assert_eq!(meta.message(), Some("hi"));
547    }
548
549    #[test]
550    fn parse_error_metadata_header_takes_priority() {
551        let proto = AwsJsonRpcProtocol::aws_json_1_1();
552        let response = http_response(
553            &[("x-amzn-errortype", "FromHeader")],
554            r#"{"__type":"FromBody","message":"go"}"#,
555        );
556        let cfg = ConfigBag::base();
557        let meta = proto.parse_error_metadata(&response, &cfg).unwrap().build();
558        assert_eq!(meta.code(), Some("FromHeader"));
559        assert_eq!(meta.message(), Some("go"));
560    }
561
562    #[test]
563    fn parse_error_metadata_sanitizes_namespaced_code() {
564        let proto = AwsJsonRpcProtocol::aws_json_1_0();
565        let response = http_response(&[], r#"{"__type":"aws.protocoltests.json#FooError"}"#);
566        let cfg = ConfigBag::base();
567        let meta = proto.parse_error_metadata(&response, &cfg).unwrap().build();
568        assert_eq!(meta.code(), Some("FooError"));
569    }
570
571    #[test]
572    fn parse_error_metadata_empty_body_returns_empty_builder() {
573        let proto = AwsJsonRpcProtocol::aws_json_1_0();
574        let response = http_response(&[], "");
575        let cfg = ConfigBag::base();
576        let meta = proto.parse_error_metadata(&response, &cfg).unwrap().build();
577        assert!(meta.code().is_none());
578        assert!(meta.message().is_none());
579    }
580
581    #[test]
582    fn parse_error_metadata_malformed_body_returns_error() {
583        let proto = AwsJsonRpcProtocol::aws_json_1_0();
584        let response = http_response(&[], r#"{"__type":"FooError""#); // truncated
585        let cfg = ConfigBag::base();
586        let err = proto.parse_error_metadata(&response, &cfg).unwrap_err();
587        assert!(matches!(err, SerdeError::InvalidInput { .. }));
588    }
589
590    #[test]
591    fn with_default_namespace_propagates_to_codec_settings() {
592        // The protocol's `with_default_namespace` builder must surface
593        // the namespace on the codec's settings — this is the wiring
594        // codegen relies on so that wire-bytes `__type:"Capacity"`
595        // lifts to a fully-qualified `com.amazonaws.dynamodb#Capacity`
596        // discriminator on the resulting [`DiscriminatedDocument`].
597        let proto = AwsJsonRpcProtocol::aws_json_1_0()
598            .with_target_prefix("DynamoDB_20120810")
599            .with_default_namespace("com.amazonaws.dynamodb");
600        assert_eq!(
601            proto.inner.codec().settings().default_namespace(),
602            Some("com.amazonaws.dynamodb"),
603        );
604    }
605
606    #[test]
607    fn with_default_namespace_preserves_other_settings() {
608        // Sanity-check that rebuilding the codec to set
609        // `default_namespace` doesn't reset other configured fields
610        // — the AwsJsonRpc constructor already disables `@jsonName`
611        // and sets epoch-seconds as the default timestamp format.
612        let proto = AwsJsonRpcProtocol::aws_json_1_0().with_default_namespace("com.example");
613        let settings = proto.inner.codec().settings();
614        assert_eq!(settings.default_namespace(), Some("com.example"));
615        assert_eq!(
616            settings.default_timestamp_format(),
617            aws_smithy_types::date_time::Format::EpochSeconds,
618        );
619    }
620
621    /// awsJson is an RPC protocol, so it must ignore HTTP response bindings — including on the
622    /// config-bag namespace branch, which builds its own body deserializer instead of delegating
623    /// to [`HttpRpcProtocol`].
624    ///
625    /// The delegating branch inherits its behavior from
626    /// `HttpRpcProtocol::deserialize_response_ignores_http_response_bindings`, but this branch
627    /// does not, so it needs its own guard. This is the deliberate mirror image of restJson1's
628    /// `the_namespace_branch_still_reads_modeled_headers_and_status`: the same branch structure,
629    /// the same schema traits, opposite required outcome. Which one applies is decided by the
630    /// protocol selected at runtime, which is the point of the whole arrangement.
631    #[test]
632    fn the_namespace_branch_still_ignores_http_response_bindings() {
633        use aws_smithy_runtime_api::http::{Response, StatusCode};
634        use aws_smithy_schema::protocol::ServiceShapeNamespace;
635        use aws_smithy_types::body::SdkBody;
636
637        static NS_NAME: Schema<'static> =
638            Schema::new_member(shape_id!("test", "Out"), ShapeType::String, "name", 0)
639                .with_http_header("x-name");
640        static NS_STATUS: Schema<'static> =
641            Schema::new_member(shape_id!("test", "Out"), ShapeType::Integer, "status", 1)
642                .with_http_response_code();
643        static NS_OUT: Schema<'static> = Schema::new_struct(
644            shape_id!("test", "Out"),
645            ShapeType::Structure,
646            &[&NS_NAME, &NS_STATUS],
647        );
648
649        let proto = AwsJsonRpcProtocol::aws_json_1_0();
650        let mut layer = Layer::new("test");
651        layer.store_put(ServiceShapeNamespace::new("com.amazonaws.dynamodb"));
652        let cfg = ConfigBag::of_layers(vec![layer]);
653        assert!(
654            crate::protocol::codec_with_bag_namespace(proto.inner.codec(), &cfg).is_some(),
655            "precondition: the namespace branch is the one being exercised"
656        );
657
658        let mut response = Response::new(
659            StatusCode::try_from(418).unwrap(),
660            SdkBody::from(r#"{"name":"from-body"}"#),
661        );
662        response
663            .headers_mut()
664            .insert("x-name", "from-header".to_string());
665
666        let mut name = None;
667        let mut status = None;
668        proto
669            .deserialize_response(&response, &NS_OUT, &cfg)
670            .unwrap()
671            .read_struct(&NS_OUT, &mut |member, d| {
672                match member.member_name() {
673                    Some("name") => name = Some(d.read_string(member)?),
674                    Some("status") => status = Some(d.read_integer(member)?),
675                    _ => {
676                        d.skip_value()?;
677                    }
678                }
679                Ok(())
680            })
681            .unwrap();
682
683        assert_eq!(
684            name.as_deref(),
685            Some("from-body"),
686            "a bound member must come from the JSON body, never from the header"
687        );
688        assert_eq!(
689            status, None,
690            "an `@httpResponseCode` member has no body representation here, so it stays absent \
691             rather than picking up the response's status"
692        );
693    }
694}