Skip to main content

aws_smithy_schema/schema/http_protocol/
rpc.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! HTTP RPC protocol for body-only APIs.
7
8use crate::codec::{Codec, FinishSerializer};
9use crate::protocol::{apply_http_endpoint, ClientProtocolInner};
10use crate::serde::{SerdeError, SerializableStruct, ShapeDeserializer, ShapeSerializer};
11use crate::{Schema, ShapeId};
12use aws_smithy_runtime_api::http::{Request, Response};
13use aws_smithy_types::body::SdkBody;
14use aws_smithy_types::config_bag::ConfigBag;
15
16/// An HTTP protocol for RPC-style APIs that put everything in the body.
17///
18/// This protocol ignores HTTP binding traits and serializes the entire input
19/// into the request body using the provided codec. Used by protocols like
20/// `awsJson1_0`, `awsJson1_1`, and `rpcv2Cbor`.
21///
22/// # Type parameters
23///
24/// * `C` — the payload codec (ex: `JsonCodec`, `CborCodec`)
25#[derive(Debug)]
26pub struct HttpRpcProtocol<C> {
27    protocol_id: ShapeId<'static>,
28    codec: C,
29    content_type: &'static str,
30}
31
32impl<C: Codec> HttpRpcProtocol<C> {
33    /// Creates a new HTTP RPC protocol.
34    pub fn new(protocol_id: ShapeId<'static>, codec: C, content_type: &'static str) -> Self {
35        Self {
36            protocol_id,
37            codec,
38            content_type,
39        }
40    }
41
42    /// Returns a reference to the body codec. Used by wrapper protocols
43    /// that need to read the codec's settings before rebuilding it via
44    /// [`Self::with_codec`].
45    pub fn codec(&self) -> &C {
46        &self.codec
47    }
48
49    /// Returns the Content-Type string this protocol stamps onto the
50    /// outgoing request. Used by wrapper protocols that rebuild the
51    /// inner [`HttpRpcProtocol`] when reconfiguring the codec.
52    pub fn content_type(&self) -> &'static str {
53        self.content_type
54    }
55
56    /// Replaces the body codec, returning a new protocol instance
57    /// with all other fields preserved. Used by wrapper protocols
58    /// (e.g. AWS JSON RPC) that need to swap in a reconfigured codec.
59    pub fn with_codec(self, codec: C) -> Self {
60        Self {
61            protocol_id: self.protocol_id,
62            codec,
63            content_type: self.content_type,
64        }
65    }
66}
67
68impl<C> ClientProtocolInner for HttpRpcProtocol<C>
69where
70    C: Codec + Send + Sync + std::fmt::Debug + 'static,
71    for<'a> C::Deserializer<'a>: ShapeDeserializer,
72{
73    type Request = Request;
74    type Response = Response;
75
76    fn protocol_id(&self) -> &ShapeId<'static> {
77        &self.protocol_id
78    }
79
80    fn serialize_request(
81        &self,
82        input: &dyn SerializableStruct,
83        input_schema: &Schema<'_>,
84        endpoint: &str,
85        cfg: &ConfigBag,
86    ) -> Result<Request, SerdeError> {
87        let mut serializer = self.codec.create_serializer();
88        serializer.write_struct(input_schema, input)?;
89        let body = serializer.finish();
90
91        let mut request = Request::new(SdkBody::from(body));
92        request
93            .set_method("POST")
94            .map_err(|e| SerdeError::custom(format!("invalid HTTP method: {e}")))?;
95        let uri = if endpoint.is_empty() { "/" } else { endpoint };
96        request
97            .set_uri(uri)
98            .map_err(|e| SerdeError::custom(format!("invalid endpoint URI: {e}")))?;
99
100        // A presigning interceptor (or any other caller that stored a
101        // `SharedHeaderOmitSettings` in the config bag) can request that these
102        // protocol-default headers be suppressed so they don't end up in the
103        // signed-header set of a presigned URL. Mirrors
104        // `HttpBindingProtocol::serialize_request_with_body`.
105        let omit = cfg.load::<crate::header_omit_settings::SharedHeaderOmitSettings>();
106        let omit_content_type = omit
107            .map(|s| s.should_omit_default_content_type())
108            .unwrap_or(false);
109        let omit_content_length = omit
110            .map(|s| s.should_omit_default_content_length())
111            .unwrap_or(false);
112        if !omit_content_type {
113            request
114                .headers_mut()
115                .insert("Content-Type", self.content_type);
116        }
117        if !omit_content_length {
118            if let Some(len) = request.body().content_length() {
119                request
120                    .headers_mut()
121                    .insert("Content-Length", len.to_string());
122            }
123        }
124        Ok(request)
125    }
126
127    fn deserialize_response<'a>(
128        &self,
129        response: &'a Response,
130        _output_schema: &Schema<'_>,
131        _cfg: &'a ConfigBag,
132    ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
133        // A streaming output's body has already been taken by the caller, so
134        // `bytes()` is `None`. The `&[]` slice reads as an empty structure, so
135        // no body member is populated; for event streams the initial-response
136        // members arrive later in the first frame. HTTP bindings are
137        // deliberately ignored: this protocol is body-only.
138        let body = response.body().bytes().unwrap_or(&[]);
139        Ok(Box::new(self.codec.create_deserializer(body)))
140    }
141
142    fn payload_codec(&self) -> Option<&dyn crate::codec::DynCodec> {
143        Some(&self.codec)
144    }
145
146    fn update_endpoint(
147        &self,
148        request: &mut Request,
149        endpoint: &aws_smithy_types::endpoint::Endpoint,
150        cfg: &ConfigBag,
151    ) -> Result<(), SerdeError> {
152        apply_http_endpoint(request, endpoint, cfg)
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use crate::serde::SerializableStruct;
160    use crate::{prelude::*, ShapeType};
161
162    struct TestSerializer {
163        output: Vec<u8>,
164    }
165
166    impl FinishSerializer for TestSerializer {
167        fn finish(self) -> Vec<u8> {
168            self.output
169        }
170    }
171
172    impl ShapeSerializer for TestSerializer {
173        fn write_struct(
174            &mut self,
175            _: &Schema<'_>,
176            value: &dyn SerializableStruct,
177        ) -> Result<(), SerdeError> {
178            self.output.push(b'{');
179            value.serialize_members(self)?;
180            self.output.push(b'}');
181            Ok(())
182        }
183        fn write_list(
184            &mut self,
185            _: &Schema<'_>,
186            _: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
187        ) -> Result<(), SerdeError> {
188            Ok(())
189        }
190        fn write_map(
191            &mut self,
192            _: &Schema<'_>,
193            _: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
194        ) -> Result<(), SerdeError> {
195            Ok(())
196        }
197        fn write_boolean(&mut self, _: &Schema<'_>, _: bool) -> Result<(), SerdeError> {
198            Ok(())
199        }
200        fn write_byte(&mut self, _: &Schema<'_>, _: i8) -> Result<(), SerdeError> {
201            Ok(())
202        }
203        fn write_short(&mut self, _: &Schema<'_>, _: i16) -> Result<(), SerdeError> {
204            Ok(())
205        }
206        fn write_integer(&mut self, _: &Schema<'_>, _: i32) -> Result<(), SerdeError> {
207            Ok(())
208        }
209        fn write_long(&mut self, _: &Schema<'_>, _: i64) -> Result<(), SerdeError> {
210            Ok(())
211        }
212        fn write_float(&mut self, _: &Schema<'_>, _: f32) -> Result<(), SerdeError> {
213            Ok(())
214        }
215        fn write_double(&mut self, _: &Schema<'_>, _: f64) -> Result<(), SerdeError> {
216            Ok(())
217        }
218        fn write_big_integer(
219            &mut self,
220            _: &Schema<'_>,
221            _: &aws_smithy_types::BigInteger,
222        ) -> Result<(), SerdeError> {
223            Ok(())
224        }
225        fn write_big_decimal(
226            &mut self,
227            _: &Schema<'_>,
228            _: &aws_smithy_types::BigDecimal,
229        ) -> Result<(), SerdeError> {
230            Ok(())
231        }
232        fn write_string(&mut self, _: &Schema<'_>, v: &str) -> Result<(), SerdeError> {
233            self.output.extend_from_slice(v.as_bytes());
234            Ok(())
235        }
236        fn write_blob(
237            &mut self,
238            _: &Schema<'_>,
239            _: aws_smithy_types::Blob,
240        ) -> Result<(), SerdeError> {
241            Ok(())
242        }
243        fn write_timestamp(
244            &mut self,
245            _: &Schema<'_>,
246            _: &aws_smithy_types::DateTime,
247        ) -> Result<(), SerdeError> {
248            Ok(())
249        }
250        fn write_document(
251            &mut self,
252            _: &Schema<'_>,
253            _: &aws_smithy_types::Document,
254        ) -> Result<(), SerdeError> {
255            Ok(())
256        }
257        fn write_null(&mut self, _: &Schema<'_>) -> Result<(), SerdeError> {
258            Ok(())
259        }
260    }
261
262    struct TestDeserializer<'a> {
263        input: &'a [u8],
264    }
265
266    impl ShapeDeserializer for TestDeserializer<'_> {
267        /// Reports every member of the schema, as a real codec would for a body that carried
268        /// them all.
269        ///
270        /// A stub that ignored the consumer could not discriminate *where* a member's value
271        /// came from, which is exactly what the body-only protocol tests below need to observe:
272        /// an RPC protocol must read an `@httpHeader` member from the body representation and
273        /// never from the header.
274        fn read_struct(
275            &mut self,
276            schema: &Schema<'_>,
277            consumer: &mut dyn FnMut(
278                &Schema<'_>,
279                &mut dyn ShapeDeserializer,
280            ) -> Result<(), SerdeError>,
281        ) -> Result<(), SerdeError> {
282            for member in schema.members() {
283                consumer(member, self)?;
284            }
285            Ok(())
286        }
287        fn read_list(
288            &mut self,
289            _: &Schema<'_>,
290            _: &mut dyn FnMut(&mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
291        ) -> Result<(), SerdeError> {
292            Ok(())
293        }
294        fn read_map(
295            &mut self,
296            _: &Schema<'_>,
297            _: &mut dyn FnMut(String, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
298        ) -> Result<(), SerdeError> {
299            Ok(())
300        }
301        fn read_boolean(&mut self, _: &Schema<'_>) -> Result<bool, SerdeError> {
302            Ok(false)
303        }
304        fn read_byte(&mut self, _: &Schema<'_>) -> Result<i8, SerdeError> {
305            Ok(0)
306        }
307        fn read_short(&mut self, _: &Schema<'_>) -> Result<i16, SerdeError> {
308            Ok(0)
309        }
310        fn read_integer(&mut self, _: &Schema<'_>) -> Result<i32, SerdeError> {
311            Ok(0)
312        }
313        fn read_long(&mut self, _: &Schema<'_>) -> Result<i64, SerdeError> {
314            Ok(0)
315        }
316        fn read_float(&mut self, _: &Schema<'_>) -> Result<f32, SerdeError> {
317            Ok(0.0)
318        }
319        fn read_double(&mut self, _: &Schema<'_>) -> Result<f64, SerdeError> {
320            Ok(0.0)
321        }
322        fn read_big_integer(
323            &mut self,
324            _: &Schema<'_>,
325        ) -> Result<aws_smithy_types::BigInteger, SerdeError> {
326            use std::str::FromStr;
327            Ok(aws_smithy_types::BigInteger::from_str("0").unwrap())
328        }
329        fn read_big_decimal(
330            &mut self,
331            _: &Schema<'_>,
332        ) -> Result<aws_smithy_types::BigDecimal, SerdeError> {
333            use std::str::FromStr;
334            Ok(aws_smithy_types::BigDecimal::from_str("0").unwrap())
335        }
336        fn read_string(&mut self, _: &Schema<'_>) -> Result<String, SerdeError> {
337            Ok(String::from_utf8_lossy(self.input).into_owned())
338        }
339        fn read_blob(&mut self, _: &Schema<'_>) -> Result<aws_smithy_types::Blob, SerdeError> {
340            Ok(aws_smithy_types::Blob::new(vec![]))
341        }
342        fn read_timestamp(
343            &mut self,
344            _: &Schema<'_>,
345        ) -> Result<aws_smithy_types::DateTime, SerdeError> {
346            Ok(aws_smithy_types::DateTime::from_secs(0))
347        }
348        fn read_document(
349            &mut self,
350            _: &Schema<'_>,
351        ) -> Result<aws_smithy_types::Document, SerdeError> {
352            Ok(aws_smithy_types::Document::Null)
353        }
354        fn is_null(&self) -> bool {
355            false
356        }
357        fn container_size(&self) -> Option<usize> {
358            None
359        }
360    }
361
362    #[derive(Debug)]
363    struct TestCodec;
364
365    impl Codec for TestCodec {
366        type Serializer = TestSerializer;
367        type Deserializer<'a> = TestDeserializer<'a>;
368        fn create_serializer(&self) -> Self::Serializer {
369            TestSerializer { output: Vec::new() }
370        }
371        fn create_deserializer<'a>(&self, input: &'a [u8]) -> Self::Deserializer<'a> {
372            TestDeserializer { input }
373        }
374    }
375
376    static TEST_SCHEMA: Schema<'static> =
377        Schema::new(crate::shape_id!("test", "TestStruct"), ShapeType::Structure);
378
379    struct EmptyStruct;
380    impl SerializableStruct for EmptyStruct {
381        fn schema(&self) -> &Schema<'_> {
382            &TEST_SCHEMA
383        }
384
385        fn serialize_members(&self, _: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
386            Ok(())
387        }
388    }
389
390    static NAME_MEMBER: Schema<'static> = Schema::new_member(
391        crate::shape_id!("test", "TestStruct"),
392        ShapeType::String,
393        "name",
394        0,
395    );
396    static MEMBERS: &[&Schema<'_>] = &[&NAME_MEMBER];
397    static STRUCT_WITH_MEMBER: Schema<'static> = Schema::new_struct(
398        crate::shape_id!("test", "TestStruct"),
399        ShapeType::Structure,
400        MEMBERS,
401    );
402
403    struct NameStruct;
404    impl SerializableStruct for NameStruct {
405        fn schema(&self) -> &Schema<'_> {
406            &STRUCT_WITH_MEMBER
407        }
408
409        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
410            s.write_string(&NAME_MEMBER, "Alice")
411        }
412    }
413
414    // Fixtures for the body-only guard below: a structure whose members carry the response
415    // bindings an RPC protocol is required to ignore.
416    static BOUND_NAME: Schema<'static> = Schema::new_member(
417        crate::shape_id!("test", "BoundStruct"),
418        ShapeType::String,
419        "name",
420        0,
421    )
422    .with_http_header("x-name");
423    static BOUND_STATUS: Schema<'static> = Schema::new_member(
424        crate::shape_id!("test", "BoundStruct"),
425        ShapeType::Integer,
426        "status",
427        1,
428    )
429    .with_http_response_code();
430    static BOUND_MEMBERS: &[&Schema<'_>] = &[&BOUND_NAME, &BOUND_STATUS];
431    static BOUND_STRUCT: Schema<'static> = Schema::new_struct(
432        crate::shape_id!("test", "BoundStruct"),
433        ShapeType::Structure,
434        BOUND_MEMBERS,
435    );
436
437    /// A response carrying values for both bound members, so a test can tell whether the
438    /// protocol read them from the transport or from the body.
439    fn bound_response(body: &'static str) -> Response {
440        let response = http::Response::builder()
441            .status(418)
442            .header("x-name", "from-header")
443            .body(SdkBody::from(body))
444            .expect("response");
445        Response::try_from(response).expect("convertible")
446    }
447
448    fn collect<'c>(
449        strings: &'c mut Vec<(String, String)>,
450        integers: &'c mut Vec<(String, i32)>,
451    ) -> impl FnMut(&Schema<'_>, &mut dyn ShapeDeserializer) -> Result<(), SerdeError> + 'c {
452        move |member, deser| {
453            let name = member.member_name().unwrap_or("?").to_string();
454            match member.shape_type() {
455                ShapeType::Integer => integers.push((name, deser.read_integer(member)?)),
456                _ => strings.push((name, deser.read_string(member)?)),
457            }
458            Ok(())
459        }
460    }
461
462    /// An RPC protocol must ignore HTTP response bindings and read every member from the body.
463    ///
464    /// This is the complement of the REST behavior and the reason binding applicability has to
465    /// be owned by the protocol selected at runtime rather than by the generated shape: the same
466    /// schema, carrying the same `@httpHeader` and `@httpResponseCode` traits, must produce
467    /// transport-sourced members under a REST protocol and body-sourced members here. See the
468    /// codec settings table in the Serialization and Schema Decoupling SEP, under which
469    /// awsJson, awsQuery, ec2Query and rpcv2Cbor all ignore HTTP bindings.
470    #[test]
471    fn deserialize_response_ignores_http_response_bindings() {
472        let protocol = HttpRpcProtocol::new(
473            crate::shape_id!("test", "rpc"),
474            TestCodec,
475            "application/x-amz-json-1.0",
476        );
477        let response = bound_response("body-value");
478        let mut strings = Vec::new();
479        let mut integers = Vec::new();
480        protocol
481            .deserialize_response(&response, &BOUND_STRUCT, &ConfigBag::base())
482            .unwrap()
483            .read_struct(&BOUND_STRUCT, &mut collect(&mut strings, &mut integers))
484            .unwrap();
485
486        // The stub codec answers `read_string` with the body bytes, so "body-value" here means
487        // the member was read from the body. "from-header" would mean the composite had been
488        // wired in and the transport had won.
489        assert_eq!(
490            strings,
491            vec![("name".to_string(), "body-value".to_string())],
492            "a bound member must be read from the body, not from the header"
493        );
494        // Likewise the response code member: 0 is the stub codec's integer, 418 is the
495        // response's status.
496        assert_eq!(integers, vec![("status".to_string(), 0)]);
497    }
498
499    /// The error path must be body-only too, including through the default forwarding.
500    ///
501    /// `HttpRpcProtocol` does not override `deserialize_error_response`, so this exercises
502    /// [`ClientProtocolInner::deserialize_error_response`]'s default, which forwards to
503    /// `deserialize_response` against `prelude::DOCUMENT`. That forwarding must stay body-only
504    /// for an RPC protocol even though the identical default is what a REST protocol has to
505    /// avoid.
506    #[test]
507    fn deserialize_error_response_ignores_http_response_bindings() {
508        let protocol = HttpRpcProtocol::new(
509            crate::shape_id!("test", "rpc"),
510            TestCodec,
511            "application/x-amz-json-1.0",
512        );
513        let response = bound_response("body-value");
514        let mut strings = Vec::new();
515        let mut integers = Vec::new();
516        protocol
517            .deserialize_error_response(&response, &ConfigBag::base())
518            .unwrap()
519            .read_struct(&BOUND_STRUCT, &mut collect(&mut strings, &mut integers))
520            .unwrap();
521
522        assert_eq!(
523            strings,
524            vec![("name".to_string(), "body-value".to_string())],
525            "a bound error member must be read from the body, not from the header"
526        );
527        assert_eq!(integers, vec![("status".to_string(), 0)]);
528    }
529
530    #[test]
531    fn serialize_sets_content_type() {
532        let protocol = HttpRpcProtocol::new(
533            crate::shape_id!("test", "rpc"),
534            TestCodec,
535            "application/x-amz-json-1.0",
536        );
537        let request = protocol
538            .serialize_request(
539                &EmptyStruct,
540                &TEST_SCHEMA,
541                "https://example.com",
542                &ConfigBag::base(),
543            )
544            .unwrap();
545        assert_eq!(
546            request.headers().get("Content-Type").unwrap(),
547            "application/x-amz-json-1.0"
548        );
549    }
550
551    #[test]
552    fn serialize_body() {
553        let protocol = HttpRpcProtocol::new(
554            crate::shape_id!("test", "rpc"),
555            TestCodec,
556            "application/x-amz-json-1.0",
557        );
558        let request = protocol
559            .serialize_request(
560                &NameStruct,
561                &STRUCT_WITH_MEMBER,
562                "https://example.com",
563                &ConfigBag::base(),
564            )
565            .unwrap();
566        assert_eq!(request.body().bytes().unwrap(), b"{Alice}");
567    }
568
569    #[test]
570    fn serialize_empty_endpoint_defaults_to_root() {
571        let protocol = HttpRpcProtocol::new(
572            crate::shape_id!("test", "rpc"),
573            TestCodec,
574            "application/x-amz-json-1.0",
575        );
576        let request = protocol
577            .serialize_request(&EmptyStruct, &TEST_SCHEMA, "", &ConfigBag::base())
578            .unwrap();
579        assert_eq!(request.uri(), "/");
580    }
581
582    #[test]
583    fn deserialize_response() {
584        let protocol = HttpRpcProtocol::new(
585            crate::shape_id!("test", "rpc"),
586            TestCodec,
587            "application/x-amz-json-1.0",
588        );
589        let response = Response::new(
590            200u16.try_into().unwrap(),
591            SdkBody::from(r#"{"result":42}"#),
592        );
593        let base_cfg = ConfigBag::base();
594        let mut deser = protocol
595            .deserialize_response(&response, &TEST_SCHEMA, &base_cfg)
596            .unwrap();
597        assert_eq!(deser.read_string(&STRING).unwrap(), r#"{"result":42}"#);
598    }
599
600    #[test]
601    fn update_endpoint() {
602        let protocol = HttpRpcProtocol::new(
603            crate::shape_id!("test", "rpc"),
604            TestCodec,
605            "application/x-amz-json-1.0",
606        );
607        let mut request = protocol
608            .serialize_request(
609                &EmptyStruct,
610                &TEST_SCHEMA,
611                "https://old.example.com",
612                &ConfigBag::base(),
613            )
614            .unwrap();
615        let endpoint = aws_smithy_types::endpoint::Endpoint::builder()
616            .url("https://new.example.com")
617            .build();
618        protocol
619            .update_endpoint(&mut request, &endpoint, &ConfigBag::base())
620            .unwrap();
621        assert_eq!(request.uri(), "https://new.example.com/");
622    }
623
624    #[test]
625    fn protocol_id() {
626        let protocol = HttpRpcProtocol::new(
627            crate::shape_id!("aws.protocols", "awsJson1_0"),
628            TestCodec,
629            "application/x-amz-json-1.0",
630        );
631        assert_eq!(protocol.protocol_id().as_str(), "aws.protocols#awsJson1_0");
632    }
633
634    #[test]
635    fn serialize_honors_header_omit_settings() {
636        use crate::header_omit_settings::{HeaderOmitSettings, SharedHeaderOmitSettings};
637        use aws_smithy_types::config_bag::Layer;
638
639        #[derive(Debug)]
640        struct OmitBoth;
641        impl HeaderOmitSettings for OmitBoth {
642            fn should_omit_default_content_type(&self) -> bool {
643                true
644            }
645            fn should_omit_default_content_length(&self) -> bool {
646                true
647            }
648        }
649
650        let protocol = HttpRpcProtocol::new(
651            crate::shape_id!("test", "rpc"),
652            TestCodec,
653            "application/x-amz-json-1.0",
654        );
655        let mut layer = Layer::new("test");
656        layer.store_put(SharedHeaderOmitSettings::new(OmitBoth));
657        let cfg = ConfigBag::of_layers(vec![layer]);
658
659        let request = protocol
660            .serialize_request(
661                &NameStruct,
662                &STRUCT_WITH_MEMBER,
663                "https://example.com",
664                &cfg,
665            )
666            .unwrap();
667        assert!(request.headers().get("Content-Type").is_none());
668        assert!(request.headers().get("Content-Length").is_none());
669    }
670}