Skip to main content

aws_smithy_schema/schema/http_protocol/
binding.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! HTTP binding protocol for REST-style APIs.
7
8use super::response::{http_error_deserializer, http_output_deserializer};
9use crate::codec::{Codec, FinishSerializer};
10use crate::protocol::{apply_http_endpoint, ClientProtocolInner};
11use crate::serde::{SerdeError, SerializableStruct, ShapeDeserializer, ShapeSerializer};
12use crate::{Schema, ShapeId};
13use aws_smithy_runtime_api::http::{Headers, Request, Response};
14use aws_smithy_types::body::SdkBody;
15use aws_smithy_types::config_bag::ConfigBag;
16use std::borrow::Cow;
17use std::cell::Cell;
18
19/// An HTTP protocol for REST-style APIs that use HTTP bindings.
20///
21/// This protocol splits input members between HTTP locations (headers, query
22/// strings, URI labels) and the payload based on HTTP binding traits
23/// (`@httpHeader`, `@httpQuery`, `@httpLabel`, `@httpPayload`, etc.).
24/// Non-bound members are serialized into the body using the provided codec.
25///
26/// # Type parameters
27///
28/// * `C` — the payload codec (e.g., `JsonCodec`, `XmlCodec`)
29#[derive(Debug)]
30pub struct HttpBindingProtocol<C> {
31    protocol_id: ShapeId<'static>,
32    codec: C,
33    content_type: &'static str,
34}
35
36impl<C: Codec> HttpBindingProtocol<C> {
37    /// Creates a new HTTP binding protocol.
38    pub fn new(protocol_id: ShapeId<'static>, codec: C, content_type: &'static str) -> Self {
39        Self {
40            protocol_id,
41            codec,
42            content_type,
43        }
44    }
45
46    /// Returns a reference to the body codec. Used by wrapper protocols
47    /// (e.g. `AwsRestXmlProtocol`) that need to construct and pre-configure
48    /// a body serializer before delegating to
49    /// [`serialize_request_with_body`](Self::serialize_request_with_body).
50    pub fn codec(&self) -> &C {
51        &self.codec
52    }
53
54    /// Returns the Content-Type string this protocol stamps onto the
55    /// outgoing request. Used by wrapper protocols that rebuild the
56    /// inner [`HttpBindingProtocol`] when reconfiguring the codec.
57    pub fn content_type(&self) -> &'static str {
58        self.content_type
59    }
60
61    /// Replaces the body codec, returning a new protocol instance
62    /// with all other fields preserved. Used by wrapper protocols
63    /// (e.g. AWS REST JSON) that need to swap in a reconfigured codec.
64    pub fn with_codec(self, codec: C) -> Self {
65        Self {
66            protocol_id: self.protocol_id,
67            codec,
68            content_type: self.content_type,
69        }
70    }
71
72    /// Body-providable variant of [`serialize_request`](Self::serialize_request).
73    /// The caller supplies an already-constructed body serializer, allowing
74    /// codec-specific pre-configuration (e.g. setting one-shot state on the
75    /// body codec before binding-driven serialization begins). The default
76    /// `serialize_request` implementation calls this with a fresh serializer
77    /// from the codec.
78    ///
79    /// This is the extension point used by `AwsRestXmlProtocol` to inject the
80    /// member-level `@xmlName` for an `@httpPayload` struct member into the
81    /// body codec — a value the codec couldn't otherwise see because codegen
82    /// passes the *target* shape's `SCHEMA` for that member, which carries
83    /// the target's `@xmlName` but not the member's.
84    pub fn serialize_request_with_body(
85        &self,
86        body: <C as Codec>::Serializer,
87        input: &dyn SerializableStruct,
88        input_schema: &Schema<'_>,
89        endpoint: &str,
90        cfg: &ConfigBag,
91    ) -> Result<Request, SerdeError> {
92        // Construct the request up front with an empty body. The binder is
93        // given a `&mut Headers` reference into this request and inserts
94        // headers directly as it walks members — avoiding the cost of an
95        // intermediate `Vec<(...)>` plus a late flush loop. The body and URI
96        // are populated after the binder's borrow is released.
97        let mut request = Request::new(SdkBody::empty());
98
99        // Check if there's an @httpPayload member targeting a structure/union.
100        // In that case, the payload member's own write_struct provides the body
101        // framing, so we must not add top-level struct framing.
102        //
103        // Codegen records this on the schema so the common path is a single
104        // discriminant read. `PayloadHint::Unknown`, every hand-constructed or
105        // runtime-materialized schema, and every crate generated before codegen
106        // began recording the hint. It falls back to deriving it by scanning
107        // members, which is what this did unconditionally before. The two
108        // branches must stay in agreement: `PayloadHint` is an optimization, not
109        // a correctness input.
110        let has_struct_payload = match input_schema.payload_hint() {
111            crate::PayloadHint::StructPayload => true,
112            crate::PayloadHint::NoStructPayload => false,
113            _ => input_schema.members().iter().any(|m| {
114                m.http_payload().is_some()
115                    && matches!(
116                        m.shape_type(),
117                        crate::ShapeType::Structure | crate::ShapeType::Union
118                    )
119            }),
120        };
121        // If the schema declares zero body members (every member is HTTP-bound,
122        // and any `@httpPayload` is on a scalar that bypasses the codec),
123        // we can skip body-codec invocation entirely. The wasted work would be:
124        //   - XmlSerializer/JsonSerializer::write_struct opens a wrapper element
125        //   - Proxy::serialize_members routes members through a BindingRouter
126        //   - close-element is emitted
127        //   - bytes are collected by `body.finish()` and then discarded
128        //     (since `has_body_members == false` later forces `body = Vec::new()`)
129        // Skipping all of that just calls `serialize_members` directly through
130        // a `BindingRouter` so HTTP-bound members are still routed to headers /
131        // query / labels. No `Proxy` is involved, so nothing can emit framing;
132        // a nested struct that happened to pass through (none do in this branch
133        // by definition) would delegate straight to the body serializer.
134        //
135        // Codegen sets `with_no_body_members()` on operation input shapes whose
136        // members are all HTTP-bound (e.g., S3 PutObjectInput, CopyObjectInput).
137        // Hand-constructed schemas default to `has_body_members == true` so this
138        // optimization is never silently applied to a schema that actually has
139        // body members.
140        let skip_body_codec = !input_schema.has_body_members() && !has_struct_payload;
141
142        // Run the binder in a scope so its `&mut Headers` borrow on `request`
143        // is released before we mutate the request again (set_uri / body swap
144        // / Content-Type / Content-Length).
145        let (raw_payload, body_bytes, query_params, labels) = {
146            let mut body = body;
147            let mut state = BindingState::new(Some(input_schema), request.headers_mut());
148
149            if skip_body_codec || has_struct_payload {
150                // skip_body_codec: input has no body members at all → all members
151                //                  route to HTTP bindings, body bytes are unused.
152                // has_struct_payload: an @httpPayload struct member writes itself
153                //                     to the body without wrapping — call
154                //                     serialize_members directly so framing comes
155                //                     from the payload struct, not from the codec.
156                //
157                // No codec framing is wanted, so route members directly and skip
158                // the `Proxy` entirely.
159                let mut router = BindingRouter {
160                    state: &mut state,
161                    body: &mut body,
162                };
163                input.serialize_members(&mut router)?;
164            } else {
165                // Framing comes from the codec: hand it a `Proxy` in place of the
166                // real input so that `{`/`}` (JSON) or the enclosing element (XML)
167                // is emitted by the codec, while each member still routes through
168                // `BindingRouter`.
169                //
170                // `state` is lent to the proxy for the duration of this call only.
171                // The disjoint borrows of `state` and `body` are what make this
172                // safe without a raw pointer; see `BindingState`'s documentation
173                // for the aliasing UB this replaced.
174                let proxy = Proxy {
175                    state: Cell::new(Some(&mut state)),
176                    value: input,
177                };
178                body.write_struct(input_schema, &proxy)?;
179            }
180            let raw_payload = state.raw_payload;
181            let body_bytes = if raw_payload.is_some() || skip_body_codec {
182                // @httpPayload blob/string — don't use the codec output.
183                // skip_body_codec — body codec was never written to.
184                Vec::new()
185            } else {
186                body.finish()
187            };
188            (raw_payload, body_bytes, state.query_params, state.labels)
189        };
190
191        // Per the REST-JSON content-type handling spec:
192        // - If @httpPayload targets a blob/string: send raw bytes, no Content-Type when empty
193        // - If body members exist (even if all optional and unset): send `{}` with Content-Type
194        // - If no body members at all (everything is in headers/query/labels): empty body, no Content-Type
195        let has_blob_or_string_payload = raw_payload.is_some();
196        // Mirror the schema's compile-time signal at runtime. When the schema
197        // says no body members AND there's no struct-payload override, this
198        // is straightforwardly false.
199        let has_body_members = has_struct_payload
200            || (input_schema.has_body_members()
201                && input_schema.members().iter().any(|m| {
202                    m.http_header().is_none()
203                        && m.http_query().is_none()
204                        && m.http_label().is_none()
205                        && m.http_prefix_headers().is_none()
206                        && m.http_query_params().is_none()
207                        && m.http_payload().is_none()
208                }));
209
210        let mut body_bytes = body_bytes;
211        let set_content_type = if has_blob_or_string_payload {
212            // Blob/string payload: Content-Type comes from the @httpHeader("Content-Type")
213            // member if present, or defaults to application/octet-stream for blobs.
214            // Don't set the protocol's codec content type (e.g., application/json).
215            false
216        } else if has_body_members {
217            // Operation has body members — body includes framing (e.g., `{}`).
218            // Per the REST-JSON spec, even if all members are optional and unset, send `{}`.
219            true
220        } else {
221            // No body members at all — empty body, no Content-Type.
222            body_bytes = Vec::new();
223            false
224        };
225
226        // Build URI: write directly into a single, capacity-hinted String
227        // instead of repeatedly `format!`-allocating placeholders and
228        // `replace`-allocating new copies of the path. Profiling on PutObject
229        // SER showed `format::format_inner` + `alloc::str::replace` together
230        // were ~25% of bench loop. The new path is one allocation per request
231        // for the URI string itself; percent-encoding writes through
232        // `percent_encode_into` to avoid per-segment String allocs.
233        let template_opt = input_schema.http().map(|h| h.uri());
234        // Capacity heuristic: template + slack for label expansion (greedy
235        // labels typically expand by O(1.5x)), or the endpoint when it is
236        // serving as the template. Better-than-default initial capacity avoids
237        // the first 1-2 reallocs.
238        let mut uri =
239            String::with_capacity(endpoint.len() + template_opt.map(|t| t.len()).unwrap_or(1) + 64);
240        match template_opt {
241            Some(template) => {
242                // The `@http` template is authoritative and `endpoint` is ignored. A REST protocol
243                // owns its route, so a path computed for a different protocol must not be prefixed
244                // onto it — an rpcv2Cbor-generated client that selects restJson1 at runtime passes
245                // `/service/{service}/operation/{operation}` here, and awsJson passes `/`.
246                // Generated REST clients pass `""`, so this costs them nothing.
247                //
248                // This mirrors the assertion `AwsJsonRpcProtocol` and `AwsQueryProtocol` make about
249                // their own fixed routes. See `ClientProtocolInner::serialize_request` for the
250                // general rule that `endpoint` is advisory.
251                append_uri_with_labels(template, &labels, &mut uri)?;
252            }
253            None => {
254                if endpoint.is_empty() {
255                    uri.push('/');
256                } else {
257                    // Endpoint may contain `{...}` label placeholders to
258                    // substitute (this branch is for shapes without an
259                    // `@http` trait, where the endpoint *is* the template).
260                    append_uri_with_labels(endpoint, &labels, &mut uri)?;
261                }
262            }
263        }
264        if !query_params.is_empty() {
265            uri.push(if uri.contains('?') { '&' } else { '?' });
266            let mut first = true;
267            for (k, v) in &query_params {
268                if !first {
269                    uri.push('&');
270                }
271                percent_encode_into(k, &mut uri);
272                uri.push('=');
273                percent_encode_into(v, &mut uri);
274                first = false;
275            }
276        }
277
278        // Swap the body in place. Headers were inserted directly during the
279        // binder phase, so no flush loop is needed here.
280        *request.body_mut() = if let Some(payload) = raw_payload {
281            // `into_bytes` unwraps the `Blob`'s `Bytes` and `SdkBody::from(Bytes)`
282            // clones the handle, so no payload bytes are copied here.
283            SdkBody::from(payload.into_bytes())
284        } else {
285            SdkBody::from(body_bytes)
286        };
287        // Set HTTP method from @http trait
288        if let Some(http) = input_schema.http() {
289            request
290                .set_method(http.method())
291                .map_err(|e| SerdeError::custom(format!("invalid HTTP method: {e}")))?;
292        }
293        request
294            .set_uri(uri.as_str())
295            .map_err(|e| SerdeError::custom(format!("invalid endpoint URI: {e}")))?;
296        // Customer-supplied @httpHeader("Content-Type") wins over the
297        // protocol default. (Pre-opt2 the late flush loop overwrote our
298        // default after we set it; with direct insertion the customer header
299        // is already present, so we must not clobber it.)
300        //
301        // A presigning interceptor (or any other caller that stored a
302        // `SharedHeaderOmitSettings` in the config bag) can request the
303        // runtime suppress these defaults so they don't end up in the signed-
304        // header set of a presigned URL.
305        let omit = cfg.load::<crate::header_omit_settings::SharedHeaderOmitSettings>();
306        let omit_content_type = omit
307            .map(|s| s.should_omit_default_content_type())
308            .unwrap_or(false);
309        let omit_content_length = omit
310            .map(|s| s.should_omit_default_content_length())
311            .unwrap_or(false);
312        if !omit_content_type && set_content_type && request.headers().get("Content-Type").is_none()
313        {
314            request
315                .headers_mut()
316                .insert("Content-Type", self.content_type);
317        }
318        if !omit_content_length {
319            if let Some(len) = request.body().content_length() {
320                if (len > 0 || set_content_type)
321                    && request.headers().get("Content-Length").is_none()
322                {
323                    request
324                        .headers_mut()
325                        .insert("Content-Length", len.to_string());
326                }
327            }
328        }
329        Ok(request)
330    }
331}
332
333// Note: there is a percent_encoding crate we use some other places for this, but I'm trying to keep
334// the dependencies to a minimum.
335/// Percent-encode a string per RFC 3986 section 2.3 (unreserved characters only).
336pub fn percent_encode(input: &str) -> String {
337    let mut out = String::with_capacity(input.len());
338    percent_encode_into(input, &mut out);
339    out
340}
341
342/// Percent-encode `input` per RFC 3986 section 2.3 (unreserved characters only),
343/// appending the result to `out`. Bulk-copies runs of already-safe bytes via
344/// `push_str` instead of pushing one byte at a time, which is the common case
345/// for URI labels and query values (typical inputs need no escaping).
346pub(crate) fn percent_encode_into(input: &str, out: &mut String) {
347    let bytes = input.as_bytes();
348    let mut start = 0usize;
349    for (i, &b) in bytes.iter().enumerate() {
350        let safe = matches!(
351            b,
352            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~'
353        );
354        if !safe {
355            // Bulk-copy the run of safe bytes ending just before `i`.
356            // SAFETY: `start..i` is a slice of `input`'s UTF-8 bytes, and
357            // every byte in `start..i` was confirmed `safe` (ASCII), so the
358            // slice is valid UTF-8.
359            if start < i {
360                out.push_str(&input[start..i]);
361            }
362            out.push('%');
363            out.push(char::from(HEX[(b >> 4) as usize]));
364            out.push(char::from(HEX[(b & 0x0f) as usize]));
365            start = i + 1;
366        }
367    }
368    if start < bytes.len() {
369        out.push_str(&input[start..]);
370    }
371}
372
373/// Walk a URI template like `/{Bucket}/{Key+}` and append the substituted
374/// path to `out`. `labels` is a small list (typically <4) so a linear
375/// scan per label-site is fine. Greedy labels (`{Name+}`) preserve `/`
376/// separators and percent-encode each segment independently; regular
377/// labels percent-encode the value as a whole.
378///
379/// Replaces an older implementation that did
380/// `path.replace(&format!("{{{name}}}"), ...)` per label — multiple
381/// String allocations per label and quadratic full-string scans. Top
382/// hot path on PutObject SER (~25% of bench loop pre-fix).
383///
384/// A template label with no value, or an empty one, is an error, as in the generated
385/// (non-schema) request serializer: substituting nothing would collapse the path segment, so
386/// `/{Bucket}/{Key+}` with an empty bucket would silently become `//key`. A greedy label whose
387/// value has an empty inner segment (`a//b`) is still allowed.
388fn append_uri_with_labels<'sc>(
389    template: &str,
390    labels: &[(Cow<'sc, str>, String)],
391    out: &mut String,
392) -> Result<(), SerdeError> {
393    let mut rem = template;
394    while let Some(open) = rem.find('{') {
395        out.push_str(&rem[..open]);
396        let after_open = &rem[open + 1..];
397        let close = match after_open.find('}') {
398            Some(c) => c,
399            None => {
400                // Malformed template (unmatched `{`); preserve verbatim.
401                out.push('{');
402                rem = after_open;
403                continue;
404            }
405        };
406        let label = &after_open[..close];
407        let (name, greedy) = match label.strip_suffix('+') {
408            Some(n) => (n, true),
409            None => (label, false),
410        };
411        // Linear lookup — labels.len() is typically <= 4.
412        let value = labels
413            .iter()
414            .find(|(n, _)| n.as_ref() == name)
415            .map(|(_, v)| v.as_str());
416        let v = match value {
417            Some(v) if !v.is_empty() => v,
418            _ => {
419                return Err(SerdeError::invalid_input(format!(
420                    "{name} was missing: cannot be empty or unset"
421                )))
422            }
423        };
424        if greedy {
425            // Encode each `/`-separated segment independently to preserve `/`.
426            let mut first = true;
427            for seg in v.split('/') {
428                if !first {
429                    out.push('/');
430                }
431                percent_encode_into(seg, out);
432                first = false;
433            }
434        } else {
435            percent_encode_into(v, out);
436        }
437        rem = &after_open[close + 1..];
438    }
439    if !rem.is_empty() {
440        out.push_str(rem);
441    }
442    Ok(())
443}
444
445pub(crate) const HEX: &[u8; 16] = b"0123456789ABCDEF";
446
447/// The HTTP-binding half of request serialization: everything a member can be
448/// routed *to* other than the body.
449///
450/// Deliberately holds no body serializer. Keeping the two apart is what makes
451/// the routing sound: [`BindingRouter`] borrows this and the body codec as two
452/// disjoint fields, so the compiler proves they cannot alias. An earlier version
453/// combined them into one `HttpBindingSerializer` that owned the codec, which
454/// forced the re-entrant `serialize_members` call to resurrect the combined
455/// object through a `*mut` derived from a shared reference — undefined behavior
456/// under both Stacked Borrows and Tree Borrows, and reproducible under Miri from
457/// this crate's own tests. Do not reunite these two halves.
458///
459/// Members without HTTP binding traits never reach this type; [`BindingRouter`]
460/// forwards them to the body serializer.
461struct BindingState<'a> {
462    /// Headers are inserted directly into the `Request`'s header map as they
463    /// are encountered, avoiding the cost of a `Vec<(...)>` intermediate plus
464    /// a late flush loop. The borrow ends when this state is dropped at the
465    /// end of `serialize_request_with_body`'s binder scope.
466    headers: &'a mut Headers,
467    query_params: Vec<(Cow<'a, str>, String)>,
468    labels: Vec<(Cow<'a, str>, String)>,
469    /// When set, member schemas are resolved from this schema by name to find
470    /// HTTP binding traits. This allows the protocol to override bindings
471    /// (e.g., for presigning where body members become query params).
472    input_schema: Option<&'a Schema<'a>>,
473    /// Raw payload bytes for `@httpPayload` blob/string members. When a member
474    /// has `@httpPayload` and targets a blob or string, the raw bytes bypass
475    /// the codec serializer entirely and are used as the HTTP body directly.
476    ///
477    /// Owned rather than borrowed. `ShapeSerializer::write_blob` takes an owned
478    /// [`Blob`](aws_smithy_types::Blob), which is `bytes::Bytes`-backed, so storing
479    /// a blob payload here is a refcount bump and handing it to `SdkBody` is
480    /// another — no payload copy anywhere on the blob path. A string payload does
481    /// cost one copy, because `write_string` takes `&str` and `str` has no shared
482    /// representation.
483    ///
484    /// This field used to be `Option<&'a [u8]>`, populated by transmuting the
485    /// `write_*` argument's anonymous lifetime to `'a`. That was unsound: `'a`
486    /// is tied to the input schema and headers, not to the serialized value, so
487    /// a caller whose `serialize_members` wrote a locally-computed payload
488    /// produced a use-after-free. Do not reintroduce a borrow here.
489    raw_payload: Option<aws_smithy_types::Blob>,
490    /// Tracks member indices that have already been routed to HTTP bindings
491    /// (`@httpHeader`, `@httpQuery`, `@httpLabel`, `@httpPrefixHeaders`,
492    /// `@httpQueryParams`). Some body codecs (notably `XmlSerializer`) call
493    /// `serialize_members` more than once on a single struct (a two-pass for
494    /// attribute / element ordering). Without this guard each HTTP-bound
495    /// member would be appended to its target collection on every pass,
496    /// duplicating header / query entries and breaking presigning signatures.
497    ///
498    /// Implementation: see [`VisitedMembers`]. Stack-only for shapes with up
499    /// to `VisitedMembers::INLINE_CAPACITY` HTTP-bound members; the JSON
500    /// path (single-pass codec, never observes a duplicate) therefore pays
501    /// no per-request allocation here.
502    visited_bound_members: VisitedMembers,
503}
504
505/// Compact dedup set for member indices seen during HTTP-binding routing.
506///
507/// Replaces `HashSet<usize>` for two reasons:
508/// 1. `HashSet::new()` is zero-alloc, but the first `.insert()` allocates
509///    the bucket array. Single-pass codecs (e.g. `JsonSerializer`) call
510///    `should_route_binding` once per HTTP-bound member but never observe a
511///    duplicate, so that allocation is pure waste on a hot path.
512/// 2. For typical structs (≤ `INLINE_CAPACITY` HTTP-bound members), inline
513///    storage avoids the heap entirely and a linear scan over `u32`s is
514///    cheaper than a hash + bucket lookup.
515///
516/// Spills to a `Vec` for larger structures (rare — S3 `CopyObject`'s
517/// 22-binding worst case still fits within `INLINE_CAPACITY`).
518#[derive(Debug)]
519struct VisitedMembers {
520    inline: [u32; Self::INLINE_CAPACITY],
521    inline_len: u8,
522    overflow: Vec<u32>,
523}
524
525impl VisitedMembers {
526    /// Sized to cover the widest real Smithy operation input shapes we
527    /// know about (S3 `CopyObject`: 22 HTTP-bound members) without
528    /// spilling to the heap. The dedup logic stays correct beyond this
529    /// limit; only the no-allocation property is lost.
530    const INLINE_CAPACITY: usize = 24;
531
532    const fn new() -> Self {
533        Self {
534            inline: [0; Self::INLINE_CAPACITY],
535            inline_len: 0,
536            overflow: Vec::new(),
537        }
538    }
539
540    /// Record `idx` as visited. Returns `true` if newly inserted, `false`
541    /// if already present (matching `HashSet::insert`'s semantics).
542    fn insert(&mut self, idx: usize) -> bool {
543        // Cap the cast at u32::MAX. Smithy member indices in practice are
544        // tiny (the largest model in the AWS catalog has fewer than 1000
545        // members on any single shape), so the truncation is unreachable
546        // except in pathological hand-constructed schemas — in which case
547        // dedup is a no-op for the truncated indices, which is correct
548        // behavior (no duplicate routing) at the cost of a possible spurious
549        // re-route.
550        let idx = idx.min(u32::MAX as usize) as u32;
551        let len = self.inline_len as usize;
552        if self.inline[..len].contains(&idx) {
553            return false;
554        }
555        if !self.overflow.is_empty() && self.overflow.contains(&idx) {
556            return false;
557        }
558        if len < Self::INLINE_CAPACITY {
559            self.inline[len] = idx;
560            self.inline_len += 1;
561        } else {
562            self.overflow.push(idx);
563        }
564        true
565    }
566}
567
568impl<'a> BindingState<'a> {
569    fn new(input_schema: Option<&'a Schema<'a>>, headers: &'a mut Headers) -> Self {
570        Self {
571            headers,
572            query_params: Vec::new(),
573            labels: Vec::new(),
574            input_schema,
575            raw_payload: None,
576            visited_bound_members: VisitedMembers::new(),
577        }
578    }
579
580    /// Returns `true` the first time this member's HTTP binding is observed
581    /// on this serializer, marking it visited. Some body codecs (notably
582    /// `XmlSerializer`) invoke `serialize_members` more than once on the same
583    /// struct (a two-pass for attribute / element ordering). Without this
584    /// guard each HTTP-bound member would be appended to its target
585    /// collection on every pass, duplicating header / query / label entries
586    /// and producing wrong-signed presigned URLs.
587    ///
588    /// HTTP-bound members are always struct members and so always have an
589    /// index. The `unwrap_or(true)` fallback for schemas without an index
590    /// keeps the helper conservative — it routes when it can't dedupe.
591    fn should_route_binding(&mut self, schema: &Schema<'_>) -> bool {
592        schema
593            .member_index()
594            .map(|idx| self.visited_bound_members.insert(idx))
595            .unwrap_or(true)
596    }
597
598    /// Resolve the effective member schema: if an input_schema override is set,
599    /// look up the member by name there (to get the correct HTTP bindings).
600    /// Otherwise use the schema as-is.
601    fn resolve_member<'s>(&self, schema: &'s Schema<'s>) -> &'s Schema<'s>
602    where
603        'a: 's,
604    {
605        if let (Some(input_schema), Some(idx)) = (self.input_schema, schema.member_index()) {
606            input_schema.member_schema_by_index(idx).unwrap_or(schema)
607        } else if let (Some(input_schema), Some(name)) = (self.input_schema, schema.member_name()) {
608            // Fallback to name lookup for schemas without a member index
609            input_schema.member_schema(name).unwrap_or(schema)
610        } else {
611            schema
612        }
613    }
614
615    /// Like [`Self::resolve_member`] but only succeeds when the member can be
616    /// resolved through `input_schema`. Returns the member with the
617    /// binder's `'a` data lifetime so callers can push into `'a`-bound
618    /// collections (e.g. `labels: Vec<(Cow<'a, str>, String)>`) without
619    /// allocating, even when the trait-method schema parameter's
620    /// anonymous lifetime is unrelated to `'a`.
621    fn resolve_to_input_schema(&self, schema: &Schema<'_>) -> Option<&'a Schema<'a>> {
622        let input_schema = self.input_schema?;
623        if let Some(idx) = schema.member_index() {
624            if let Some(s) = input_schema.member_schema_by_index(idx) {
625                return Some(s);
626            }
627        }
628        if let Some(name) = schema.member_name() {
629            return input_schema.member_schema(name);
630        }
631        None
632    }
633
634    /// The `@httpQuery` parameter name for a member, as a `Cow<'a, str>` so it
635    /// can be pushed into `query_params` without allocating.
636    ///
637    /// `@httpQuery` values carry the schema's data lifetime, and the schema
638    /// arriving through a `ShapeSerializer` method has an anonymous lifetime
639    /// unrelated to the binder's `'a`. Resolving the member through
640    /// `input_schema` recovers a value that lives for `'a`; when that fails
641    /// (no `input_schema`, or the member is not found there) the name is
642    /// copied. Mirrors the `@httpLabel` handling in [`Self::add_binding`].
643    fn query_param_name(
644        &self,
645        schema: &Schema<'_>,
646        query: &crate::traits::HttpQueryTrait<'_>,
647    ) -> Cow<'a, str> {
648        match self
649            .resolve_to_input_schema(schema)
650            .and_then(|resolved| resolved.http_query())
651        {
652            Some(resolved_query) => Cow::Borrowed(resolved_query.value()),
653            None => Cow::Owned(query.value().to_string()),
654        }
655    }
656}
657
658/// Inserts a header bound to `member`, returning an error rather than panicking when the
659/// name or value is not a valid HTTP header component.
660///
661/// The name of a prefix header comes from a map key in the input, and a header value can
662/// contain any character a string member allows (a newline, for instance), so either can be
663/// invalid. The error names the member and `location` (the header, or the prefix for a prefix
664/// header) but never the value, which may be sensitive.
665fn insert_header(
666    headers: &mut Headers,
667    name: Cow<'static, str>,
668    value: String,
669    member: &Schema<'_>,
670    location: std::fmt::Arguments<'_>,
671) -> Result<(), SerdeError> {
672    headers
673        .try_insert(name, value)
674        .map(|_| ())
675        .map_err(|err| header_error(member, location, err))
676}
677
678#[cold]
679fn header_error(
680    member: &Schema<'_>,
681    location: std::fmt::Arguments<'_>,
682    err: aws_smithy_runtime_api::http::HttpError,
683) -> SerdeError {
684    SerdeError::invalid_input(format!(
685        "cannot serialize {} to {location}: {err}",
686        member.member_name().unwrap_or("member"),
687    ))
688}
689
690/// The parsed [`http::HeaderName`] of a member's `@httpHeader`, cached on the member schema.
691///
692/// `Headers` converts a string name into an `http::HeaderName` on every insert. For a
693/// non-standard name, such as any `x-amz-*` header, that copies the name into a new
694/// allocation, and a name with an uppercase letter is copied once more first. Cloning a
695/// cached `HeaderName` only bumps a reference count.
696///
697/// `None` when the member has no `@httpHeader`, or its name is not a valid header name. In
698/// the second case [`insert_bound_header`] falls back to the string path, which reports the
699/// error.
700pub(crate) static HTTP_HEADER_NAME: crate::extension::SchemaExtensionKey<Option<http::HeaderName>> =
701    crate::extension::SchemaExtensionKey::new(|member| {
702        member
703            .http_header()
704            .and_then(|header| http::HeaderName::try_from(header.value()).ok())
705    });
706
707/// Inserts a value for a member bound with `@httpHeader`, using the member's cached
708/// [`HTTP_HEADER_NAME`].
709fn insert_bound_header(
710    headers: &mut Headers,
711    member: &Schema<'_>,
712    header: &crate::traits::HttpHeaderTrait<'_>,
713    value: String,
714) -> Result<(), SerdeError> {
715    match member.extension(&HTTP_HEADER_NAME) {
716        Some(name) => headers
717            .try_insert(name.clone(), value)
718            .map(|_| ())
719            .map_err(|err| header_error(member, format_args!("header `{}`", header.value()), err)),
720        None => insert_header(
721            headers,
722            header_name(header),
723            value,
724            member,
725            format_args!("header `{}`", header.value()),
726        ),
727    }
728}
729
730/// Resolves an `@httpHeader` name into the `Cow<'static, str>` that
731/// `Headers::insert` requires, for a member with no cached [`HTTP_HEADER_NAME`].
732///
733/// `value_static()` is `Some` for every schema that can be built today — the
734/// only `@httpHeader` constructor takes `&'static str` — so this is a
735/// zero-allocation borrow in practice. The owned arm exists so that relaxing
736/// `@httpHeader` to accept arena-borrowed names stays an additive change
737/// instead of breaking this call site.
738fn header_name(header: &crate::traits::HttpHeaderTrait<'_>) -> Cow<'static, str> {
739    match header.value_static() {
740        Some(name) => Cow::Borrowed(name),
741        None => Cow::Owned(header.value().to_string()),
742    }
743}
744
745/// Routes each member of the top-level input struct either into
746/// [`BindingState`] (HTTP-bound members) or into the body codec (everything
747/// else).
748///
749/// The two references are separate fields precisely so that the compiler can
750/// see they do not alias. `body` is the `&mut dyn ShapeSerializer` that the
751/// codec hands to `SerializableStruct::serialize_members`; an earlier version
752/// discarded that argument and reconstructed the body serializer from a raw
753/// pointer instead, which was the source of the aliasing UB. Use the argument.
754///
755/// The three lifetimes are load-bearing and must stay distinct. Collapsing
756/// `'s` and `'b` into one shortens the state borrow to the body serializer's
757/// and produces an unfixable variance error at the [`Cell`] in `Proxy`, because
758/// `Cell<T>` is invariant over `T`.
759struct BindingRouter<'s, 'b, 'a> {
760    state: &'s mut BindingState<'a>,
761    body: &'b mut dyn ShapeSerializer,
762}
763
764/// Bridges the codec's `write_struct` framing back into HTTP-binding routing.
765///
766/// The body codec is asked to serialize *this* rather than the real input, so
767/// that framing (`{`/`}` for JSON, the element for XML) comes from the codec
768/// while member routing still passes through [`BindingRouter`].
769///
770/// `serialize_members` receives `&self`, so the state is lent out through a
771/// [`Cell`] take/put rather than held as `&mut`. A `Cell` is used over a
772/// `RefCell` to stay allocation-free and to avoid a second panicking path; the
773/// take/put is required rather than merely convenient because some codecs
774/// (notably XML) call `serialize_members` more than once and each call needs
775/// the state back.
776struct Proxy<'p, 'a> {
777    state: Cell<Option<&'p mut BindingState<'a>>>,
778    value: &'p dyn SerializableStruct,
779}
780
781impl<'p, 'a> SerializableStruct for Proxy<'p, 'a> {
782    fn schema(&self) -> &Schema<'_> {
783        self.value.schema()
784    }
785
786    fn serialize_members(&self, serializer: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
787        // Returning an error rather than panicking: this is unreachable with
788        // every codec in this repo, because a codec that nests
789        // `serialize_members` on the *same* struct value would have to call it
790        // re-entrantly rather than sequentially. Sequential calls (XML's two
791        // passes) put the state back before the next one begins. A future codec
792        // that does nest should get a serialization error, not a panic in a
793        // customer's request path.
794        let state = self.state.take().ok_or_else(|| {
795            SerdeError::custom(
796                "HTTP binding state is already borrowed: the body codec re-entered \
797                 serialize_members on the same struct before the previous call returned",
798            )
799        })?;
800        let mut router = BindingRouter {
801            state,
802            body: serializer,
803        };
804        let result = self.value.serialize_members(&mut router);
805        // Put the state back even on the error path, so a codec that ignores
806        // one member's error and continues does not then hit the branch above.
807        self.state.set(Some(router.state));
808        result
809    }
810}
811
812impl<'s, 'b, 'a> ShapeSerializer for BindingRouter<'s, 'b, 'a> {
813    fn write_struct(
814        &mut self,
815        schema: &Schema<'_>,
816        value: &dyn SerializableStruct,
817    ) -> Result<(), SerdeError> {
818        // A nested struct (a body member targeting a structure): delegate
819        // entirely to the body serializer. Only the top-level input struct is
820        // routed, and that entry point is `serialize_request_with_body`'s
821        // explicit `Proxy` construction rather than a flag checked here.
822        let schema = self.state.resolve_member(schema);
823        // `@httpPayload` struct/union: codegen routes these by passing the
824        // target struct's schema directly (not the member schema), so the
825        // payload branch is normally unreachable. Both arms are the same call;
826        // the `if` is kept as documentation of that intent.
827        self.body.write_struct(schema, value)
828    }
829
830    fn write_list(
831        &mut self,
832        schema: &Schema<'_>,
833        write_elements: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
834    ) -> Result<(), SerdeError> {
835        let schema = self.state.resolve_member(schema);
836        // @httpHeader on a list: collect elements as comma-separated header value
837        if let Some(header) = schema.http_header() {
838            if !self.state.should_route_binding(schema) {
839                return Ok(());
840            }
841            let mut collector = ListElementCollector::for_header();
842            write_elements(&mut collector)?;
843            // RFC 7230: string values are quoted with the same rule the generated
844            // (non-schema) header serializer uses, `quote_header_value`. Timestamps are NOT
845            // quoted even though http-date contains commas; readers use `many_dates`.
846            let header_val = collector
847                .values
848                .iter()
849                .zip(collector.quotable.iter())
850                .map(|(s, &quotable)| {
851                    if quotable {
852                        aws_smithy_runtime_api::http::header_parse::quote_header_value(s.as_str())
853                            .into_owned()
854                    } else {
855                        s.clone()
856                    }
857                })
858                .collect::<Vec<_>>()
859                .join(", ");
860            insert_bound_header(self.state.headers, schema, header, header_val)?;
861            return Ok(());
862        }
863        // @httpQuery on a list: add each element as a separate query param
864        if let Some(query) = schema.http_query() {
865            if !self.state.should_route_binding(schema) {
866                return Ok(());
867            }
868            let mut collector = ListElementCollector::for_query();
869            write_elements(&mut collector)?;
870            // Prefer the `'a`-lifetime member from `input_schema` so the pushed
871            // `Cow<'a, str>` can be `Borrowed` (zero-alloc). The trait method's
872            // `&Schema<'_>` has an anonymous lifetime unrelated to `'a`, and
873            // `@httpQuery` values carry the schema's data lifetime, so we fall
874            // back to allocating when the member cannot be resolved.
875            let name = self.state.query_param_name(schema, query);
876            for val in collector.values {
877                self.state.query_params.push((name.clone(), val));
878            }
879            return Ok(());
880        }
881        self.body.write_list(schema, write_elements)
882    }
883
884    fn write_map(
885        &mut self,
886        schema: &Schema<'_>,
887        write_entries: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
888    ) -> Result<(), SerdeError> {
889        let schema = self.state.resolve_member(schema);
890        // @httpPrefixHeaders: serialize map entries as prefixed headers
891        if let Some(prefix) = schema.http_prefix_headers() {
892            if !self.state.should_route_binding(schema) {
893                return Ok(());
894            }
895            // Collect entries via a temporary serializer
896            let mut collector = MapEntryCollector::new(prefix.value().to_string());
897            write_entries(&mut collector)?;
898            // Names are dynamic (prefix + map key) — owned Strings.
899            for (k, v) in collector.entries {
900                insert_header(
901                    self.state.headers,
902                    Cow::Owned(k),
903                    v,
904                    schema,
905                    format_args!("headers with prefix `{}`", prefix.value()),
906                )?;
907            }
908            return Ok(());
909        }
910        // @httpQueryParams: serialize map entries as query params
911        if schema.http_query_params().is_some() {
912            if !self.state.should_route_binding(schema) {
913                return Ok(());
914            }
915            let mut collector = MapEntryCollector::new(String::new());
916            write_entries(&mut collector)?;
917            // Filter out keys that overlap with explicit @httpQuery params
918            // (query params take precedence over query params map entries)
919            let explicit_query_keys: Vec<&str> = self
920                .state
921                .input_schema
922                .map(|s| {
923                    s.members()
924                        .iter()
925                        .filter_map(|m| m.http_query().map(|q| q.value()))
926                        .collect()
927                })
928                .unwrap_or_default();
929            for (k, v) in collector.entries {
930                if !explicit_query_keys.contains(&k.as_str()) {
931                    self.state.query_params.push((Cow::Owned(k), v));
932                }
933            }
934            return Ok(());
935        }
936        self.body.write_map(schema, write_entries)
937    }
938
939    fn write_boolean(&mut self, schema: &Schema<'_>, value: bool) -> Result<(), SerdeError> {
940        let schema = self.state.resolve_member(schema);
941        if let Some(binding) = http_string_binding(schema) {
942            return self.state.add_binding(binding, schema, &value.to_string());
943        }
944        self.body.write_boolean(schema, value)
945    }
946
947    fn write_byte(&mut self, schema: &Schema<'_>, value: i8) -> Result<(), SerdeError> {
948        let schema = self.state.resolve_member(schema);
949        if let Some(binding) = http_string_binding(schema) {
950            return self.state.add_binding(binding, schema, &value.to_string());
951        }
952        self.body.write_byte(schema, value)
953    }
954
955    fn write_short(&mut self, schema: &Schema<'_>, value: i16) -> Result<(), SerdeError> {
956        let schema = self.state.resolve_member(schema);
957        if let Some(binding) = http_string_binding(schema) {
958            return self.state.add_binding(binding, schema, &value.to_string());
959        }
960        self.body.write_short(schema, value)
961    }
962
963    fn write_integer(&mut self, schema: &Schema<'_>, value: i32) -> Result<(), SerdeError> {
964        let schema = self.state.resolve_member(schema);
965        if let Some(binding) = http_string_binding(schema) {
966            return self.state.add_binding(binding, schema, &value.to_string());
967        }
968        self.body.write_integer(schema, value)
969    }
970
971    fn write_long(&mut self, schema: &Schema<'_>, value: i64) -> Result<(), SerdeError> {
972        let schema = self.state.resolve_member(schema);
973        if let Some(binding) = http_string_binding(schema) {
974            return self.state.add_binding(binding, schema, &value.to_string());
975        }
976        self.body.write_long(schema, value)
977    }
978
979    fn write_float(&mut self, schema: &Schema<'_>, value: f32) -> Result<(), SerdeError> {
980        let schema = self.state.resolve_member(schema);
981        if let Some(binding) = http_string_binding(schema) {
982            return self
983                .state
984                .add_binding(binding, schema, &format_float_f32(value));
985        }
986        self.body.write_float(schema, value)
987    }
988
989    fn write_double(&mut self, schema: &Schema<'_>, value: f64) -> Result<(), SerdeError> {
990        let schema = self.state.resolve_member(schema);
991        if let Some(binding) = http_string_binding(schema) {
992            return self
993                .state
994                .add_binding(binding, schema, &format_float_f64(value));
995        }
996        self.body.write_double(schema, value)
997    }
998
999    fn write_big_integer(
1000        &mut self,
1001        schema: &Schema<'_>,
1002        value: &aws_smithy_types::BigInteger,
1003    ) -> Result<(), SerdeError> {
1004        let schema = self.state.resolve_member(schema);
1005        if let Some(binding) = http_string_binding(schema) {
1006            return self.state.add_binding(binding, schema, value.as_ref());
1007        }
1008        self.body.write_big_integer(schema, value)
1009    }
1010
1011    fn write_big_decimal(
1012        &mut self,
1013        schema: &Schema<'_>,
1014        value: &aws_smithy_types::BigDecimal,
1015    ) -> Result<(), SerdeError> {
1016        let schema = self.state.resolve_member(schema);
1017        if let Some(binding) = http_string_binding(schema) {
1018            return self.state.add_binding(binding, schema, value.as_ref());
1019        }
1020        self.body.write_big_decimal(schema, value)
1021    }
1022
1023    fn write_string(&mut self, schema: &Schema<'_>, value: &str) -> Result<(), SerdeError> {
1024        let schema = self.state.resolve_member(schema);
1025        if let Some(binding) = http_string_binding(schema) {
1026            // @mediaType on a header: base64-encode the value
1027            if schema.media_type().is_some() {
1028                let encoded = aws_smithy_types::base64::encode(value.as_bytes());
1029                return self.state.add_binding(binding, schema, &encoded);
1030            }
1031            return self.state.add_binding(binding, schema, value);
1032        }
1033        if schema.http_payload().is_some() {
1034            if !self.state.should_route_binding(schema) {
1035                return Ok(());
1036            }
1037            // One copy, and it is unavoidable: `write_string` hands over a `&str`
1038            // with an anonymous lifetime that has no relationship to `'a`, and
1039            // `str` has no shared representation to take a cheap handle on. The
1040            // previous implementation transmuted the lifetime to `'a` instead,
1041            // which was a use-after-free for any caller that computed its
1042            // payload into a local. `should_route_binding` above means the XML
1043            // codec's second `serialize_members` pass returns before we get
1044            // here, so this copy happens at most once per payload.
1045            self.state.raw_payload = Some(aws_smithy_types::Blob::new(value));
1046            return Ok(());
1047        }
1048        self.body.write_string(schema, value)
1049    }
1050
1051    fn write_blob(
1052        &mut self,
1053        schema: &Schema<'_>,
1054        value: aws_smithy_types::Blob,
1055    ) -> Result<(), SerdeError> {
1056        let schema = self.state.resolve_member(schema);
1057        if schema.http_header().is_some() {
1058            if !self.state.should_route_binding(schema) {
1059                return Ok(());
1060            }
1061            let encoded = aws_smithy_types::base64::encode(value.as_ref());
1062            let header = schema.http_header().unwrap();
1063            insert_bound_header(self.state.headers, schema, header, encoded)?;
1064            return Ok(());
1065        }
1066        if schema.http_payload().is_some() {
1067            if !self.state.should_route_binding(schema) {
1068                return Ok(());
1069            }
1070            // Zero copies: `Blob` is `bytes::Bytes`-backed, so this moves a
1071            // refcounted handle, and `SdkBody::from(Bytes)` at the consume site
1072            // takes another handle rather than copying. This is the reason
1073            // `write_blob` takes an owned `Blob` — see its trait documentation.
1074            self.state.raw_payload = Some(value);
1075            return Ok(());
1076        }
1077        self.body.write_blob(schema, value)
1078    }
1079
1080    fn write_timestamp(
1081        &mut self,
1082        schema: &Schema<'_>,
1083        value: &aws_smithy_types::DateTime,
1084    ) -> Result<(), SerdeError> {
1085        let schema = self.state.resolve_member(schema);
1086        if let Some(binding) = http_string_binding(schema) {
1087            // Headers default to http-date, query/label default to date-time
1088            let format = super::timestamp_format_or(
1089                schema,
1090                match binding {
1091                    HttpBinding::Header => aws_smithy_types::date_time::Format::HttpDate,
1092                    _ => aws_smithy_types::date_time::Format::DateTime,
1093                },
1094            );
1095            let formatted = value
1096                .fmt(format)
1097                .map_err(|e| SerdeError::custom(format!("failed to format timestamp: {e}")))?;
1098            return self.state.add_binding(binding, schema, &formatted);
1099        }
1100        self.body.write_timestamp(schema, value)
1101    }
1102
1103    fn write_document(
1104        &mut self,
1105        schema: &Schema<'_>,
1106        value: &aws_smithy_types::Document,
1107    ) -> Result<(), SerdeError> {
1108        self.body.write_document(schema, value)
1109    }
1110
1111    fn write_null(&mut self, schema: &Schema<'_>) -> Result<(), SerdeError> {
1112        self.body.write_null(schema)
1113    }
1114}
1115
1116/// Which HTTP location a member is bound to.
1117///
1118/// Carries no name: every caller passes the same member schema to
1119/// [`BindingState::add_binding`], which resolves the name there. That
1120/// keeps `@httpQuery`'s schema-lifetime value out of a `'static` slot.
1121enum HttpBinding {
1122    Header,
1123    Query,
1124    Label,
1125}
1126
1127/// Determine the HTTP binding for a member schema, if any.
1128fn http_string_binding(schema: &Schema<'_>) -> Option<HttpBinding> {
1129    if schema.http_header().is_some() {
1130        return Some(HttpBinding::Header);
1131    }
1132    if schema.http_query().is_some() {
1133        return Some(HttpBinding::Query);
1134    }
1135    if schema.http_label().is_some() {
1136        return Some(HttpBinding::Label);
1137    }
1138    None
1139}
1140
1141impl<'a> BindingState<'a> {
1142    fn add_binding(
1143        &mut self,
1144        binding: HttpBinding,
1145        schema: &Schema<'_>,
1146        value: &str,
1147    ) -> Result<(), SerdeError> {
1148        // Dedupe per-member: see `should_route_binding`. Without this, a
1149        // multi-pass body codec invokes `serialize_members` more than once
1150        // and each pass would append to `headers` / `query_params` / `labels`.
1151        if !self.should_route_binding(schema) {
1152            return Ok(());
1153        }
1154        match binding {
1155            HttpBinding::Header => {
1156                if let Some(header) = schema.http_header() {
1157                    insert_bound_header(self.headers, schema, header, value.to_string())?;
1158                }
1159            }
1160            HttpBinding::Query => {
1161                if let Some(query) = schema.http_query() {
1162                    let name = self.query_param_name(schema, query);
1163                    self.query_params.push((name, value.to_string()));
1164                }
1165            }
1166            HttpBinding::Label => {
1167                // Prefer the `'a`-lifetime member from `input_schema` so the
1168                // pushed `Cow<'a, str>` can be `Borrowed` (zero-alloc). The
1169                // trait method's `&Schema<'_>` schema has an anonymous
1170                // lifetime not bounded by `'a`, so we'd otherwise have to
1171                // allocate. Falls back to allocation when no `input_schema`
1172                // is available.
1173                let cow_name = if let Some(resolved) = self.resolve_to_input_schema(schema) {
1174                    let name = resolved
1175                        .member_name()
1176                        .ok_or_else(|| SerdeError::custom("httpLabel on non-member schema"))?;
1177                    Cow::Borrowed(name)
1178                } else {
1179                    let name = schema
1180                        .member_name()
1181                        .ok_or_else(|| SerdeError::custom("httpLabel on non-member schema"))?;
1182                    Cow::Owned(name.to_string())
1183                };
1184                self.labels.push((cow_name, value.to_string()));
1185            }
1186        }
1187        Ok(())
1188    }
1189}
1190
1191/// Generates inert [`ShapeSerializer`] write methods (each returning
1192/// `Ok(())`) for the named methods. The HTTP-binding collectors below
1193/// implement only the writes that map a scalar to its string form;
1194/// every other write is a no-op. Listing those no-ops through this
1195/// macro keeps each collector's impl focused on the writes it actually
1196/// handles. Each entry is `method_name(value_arg_types...)`; methods
1197/// with no value beyond the schema (e.g. `write_null`) list no types.
1198macro_rules! noop_writes {
1199    ($($method:ident($($arg:ty),*)),+ $(,)?) => {
1200        $(
1201            fn $method(&mut self, _: &Schema<'_>, $(_: $arg),*) -> Result<(), SerdeError> {
1202                Ok(())
1203            }
1204        )+
1205    };
1206}
1207
1208/// Whether a `ListElementCollector` is gathering values for a header or query param.
1209/// Affects default timestamp format: `http-date` for headers, `date-time` for query.
1210#[derive(Copy, Clone)]
1211enum HttpListTarget {
1212    Header,
1213    Query,
1214}
1215
1216/// Collects list element values as strings for @httpHeader and @httpQuery on lists.
1217struct ListElementCollector {
1218    values: Vec<String>,
1219    /// Whether each value should be quoted if it contains commas (strings yes, timestamps no)
1220    quotable: Vec<bool>,
1221    target: HttpListTarget,
1222}
1223
1224impl ListElementCollector {
1225    fn for_header() -> Self {
1226        Self::new(HttpListTarget::Header)
1227    }
1228
1229    fn for_query() -> Self {
1230        Self::new(HttpListTarget::Query)
1231    }
1232
1233    fn new(target: HttpListTarget) -> Self {
1234        Self {
1235            values: Vec::new(),
1236            quotable: Vec::new(),
1237            target,
1238        }
1239    }
1240
1241    fn push(&mut self, value: String) {
1242        self.quotable.push(true);
1243        self.values.push(value);
1244    }
1245
1246    fn push_unquotable(&mut self, value: String) {
1247        self.quotable.push(false);
1248        self.values.push(value);
1249    }
1250}
1251
1252impl ShapeSerializer for ListElementCollector {
1253    fn write_string(&mut self, _schema: &Schema<'_>, value: &str) -> Result<(), SerdeError> {
1254        self.push(value.to_string());
1255        Ok(())
1256    }
1257    fn write_boolean(&mut self, _: &Schema<'_>, value: bool) -> Result<(), SerdeError> {
1258        self.push(value.to_string());
1259        Ok(())
1260    }
1261    fn write_byte(&mut self, _: &Schema<'_>, value: i8) -> Result<(), SerdeError> {
1262        self.push(value.to_string());
1263        Ok(())
1264    }
1265    fn write_short(&mut self, _: &Schema<'_>, value: i16) -> Result<(), SerdeError> {
1266        self.push(value.to_string());
1267        Ok(())
1268    }
1269    fn write_integer(&mut self, _: &Schema<'_>, value: i32) -> Result<(), SerdeError> {
1270        self.push(value.to_string());
1271        Ok(())
1272    }
1273    fn write_long(&mut self, _: &Schema<'_>, value: i64) -> Result<(), SerdeError> {
1274        self.push(value.to_string());
1275        Ok(())
1276    }
1277    fn write_float(&mut self, _: &Schema<'_>, value: f32) -> Result<(), SerdeError> {
1278        self.push(format_float_f32(value));
1279        Ok(())
1280    }
1281    fn write_double(&mut self, _: &Schema<'_>, value: f64) -> Result<(), SerdeError> {
1282        self.push(format_float_f64(value));
1283        Ok(())
1284    }
1285    fn write_timestamp(
1286        &mut self,
1287        schema: &Schema<'_>,
1288        value: &aws_smithy_types::DateTime,
1289    ) -> Result<(), SerdeError> {
1290        let format = super::timestamp_format_or(
1291            schema,
1292            // Default: headers use http-date, query params use date-time
1293            match self.target {
1294                HttpListTarget::Header => aws_smithy_types::date_time::Format::HttpDate,
1295                HttpListTarget::Query => aws_smithy_types::date_time::Format::DateTime,
1296            },
1297        );
1298        self.push_unquotable(
1299            value
1300                .fmt(format)
1301                .map_err(|e| SerdeError::custom(format!("failed to format timestamp: {e}")))?,
1302        );
1303        Ok(())
1304    }
1305    fn write_blob(
1306        &mut self,
1307        _schema: &Schema<'_>,
1308        value: aws_smithy_types::Blob,
1309    ) -> Result<(), SerdeError> {
1310        self.push(aws_smithy_types::base64::encode(value.as_ref()));
1311        Ok(())
1312    }
1313    // Remaining writes are no-ops for list element collection.
1314    noop_writes! {
1315        write_struct(&dyn SerializableStruct),
1316        write_list(&dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>),
1317        write_map(&dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>),
1318        write_big_integer(&aws_smithy_types::BigInteger),
1319        write_big_decimal(&aws_smithy_types::BigDecimal),
1320        write_document(&aws_smithy_types::Document),
1321        write_null(),
1322    }
1323}
1324
1325/// Format a float for HTTP headers/query/labels.
1326/// Rust's Display writes "inf"/"-inf" but HTTP requires "Infinity"/"-Infinity".
1327fn format_float_f32(value: f32) -> String {
1328    if value.is_infinite() {
1329        if value.is_sign_positive() {
1330            "Infinity".to_string()
1331        } else {
1332            "-Infinity".to_string()
1333        }
1334    } else if value.is_nan() {
1335        "NaN".to_string()
1336    } else {
1337        value.to_string()
1338    }
1339}
1340
1341fn format_float_f64(value: f64) -> String {
1342    if value.is_infinite() {
1343        if value.is_sign_positive() {
1344            "Infinity".to_string()
1345        } else {
1346            "-Infinity".to_string()
1347        }
1348    } else if value.is_nan() {
1349        "NaN".to_string()
1350    } else {
1351        value.to_string()
1352    }
1353}
1354
1355/// Collects map key-value pairs written via ShapeSerializer for
1356/// @httpPrefixHeaders and @httpQueryParams.
1357struct MapEntryCollector {
1358    prefix: String,
1359    entries: Vec<(String, String)>,
1360    pending_key: Option<String>,
1361}
1362
1363impl MapEntryCollector {
1364    fn new(prefix: String) -> Self {
1365        Self {
1366            prefix,
1367            entries: Vec::new(),
1368            pending_key: None,
1369        }
1370    }
1371}
1372
1373impl ShapeSerializer for MapEntryCollector {
1374    fn write_string(&mut self, _schema: &Schema<'_>, value: &str) -> Result<(), SerdeError> {
1375        if let Some(key) = self.pending_key.take() {
1376            self.entries
1377                .push((format!("{}{}", self.prefix, key), value.to_string()));
1378        } else {
1379            self.pending_key = Some(value.to_string());
1380        }
1381        Ok(())
1382    }
1383
1384    fn write_list(
1385        &mut self,
1386        _: &Schema<'_>,
1387        write_elements: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
1388    ) -> Result<(), SerdeError> {
1389        // Map<String, List<String>>: each list element becomes a separate entry
1390        // with the same key (for @httpQueryParams).
1391        if let Some(key) = self.pending_key.take() {
1392            let mut collector = ListElementCollector::for_query(); // query params context
1393            write_elements(&mut collector)?;
1394            for val in collector.values {
1395                self.entries.push((format!("{}{}", self.prefix, key), val));
1396            }
1397        }
1398        Ok(())
1399    }
1400    // Every other write is a no-op: HTTP-binding maps have string keys
1401    // and values, and the `write_list` above handles the
1402    // Map<String, List<String>> case for @httpQueryParams.
1403    noop_writes! {
1404        write_struct(&dyn SerializableStruct),
1405        write_map(&dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>),
1406        write_boolean(bool),
1407        write_byte(i8),
1408        write_short(i16),
1409        write_integer(i32),
1410        write_long(i64),
1411        write_float(f32),
1412        write_double(f64),
1413        write_big_integer(&aws_smithy_types::BigInteger),
1414        write_big_decimal(&aws_smithy_types::BigDecimal),
1415        write_blob(aws_smithy_types::Blob),
1416        write_timestamp(&aws_smithy_types::DateTime),
1417        write_document(&aws_smithy_types::Document),
1418        write_null(),
1419    }
1420}
1421
1422impl<C> ClientProtocolInner for HttpBindingProtocol<C>
1423where
1424    C: Codec + Send + Sync + std::fmt::Debug + 'static,
1425    for<'a> C::Deserializer<'a>: ShapeDeserializer,
1426{
1427    type Request = Request;
1428    type Response = Response;
1429
1430    fn protocol_id(&self) -> &ShapeId<'static> {
1431        &self.protocol_id
1432    }
1433
1434    fn serialize_request(
1435        &self,
1436        input: &dyn SerializableStruct,
1437        input_schema: &Schema<'_>,
1438        endpoint: &str,
1439        cfg: &ConfigBag,
1440    ) -> Result<Request, SerdeError> {
1441        let body = self.codec.create_serializer();
1442        self.serialize_request_with_body(body, input, input_schema, endpoint, cfg)
1443    }
1444
1445    /// Returns a deserializer that reads the **whole** response: `@httpHeader`,
1446    /// `@httpPrefixHeaders`, `@httpResponseCode`, and `@httpPayload` members from the HTTP
1447    /// message, everything else from the body codec.
1448    ///
1449    /// This is what makes binding applicability a property of the protocol selected at runtime
1450    /// rather than of the protocol the client was generated from. An output with no
1451    /// response-bound members still gets the bare codec deserializer.
1452    ///
1453    /// # On the body
1454    ///
1455    /// For a non-streaming response the orchestrator has already loaded the body into an
1456    /// in-memory `Once(..)`, so `bytes()` returns the payload. For a streaming response — whose
1457    /// output has a `@streaming` `@httpPayload` blob or event-stream member — the body is left
1458    /// as a `BoxBody`, possibly further wrapped by an interceptor such as
1459    /// `ResponseChecksumInterceptor`, and `bytes()` returns `None`.
1460    ///
1461    /// The codec is therefore built over `unwrap_or(&[])`, which it reads as "no body members",
1462    /// while the true `Option` is what the wrapper uses to tell an unbuffered body apart from an
1463    /// empty one. A `@streaming` payload is skipped outright, so the live body is never consumed
1464    /// here.
1465    fn deserialize_response<'a>(
1466        &self,
1467        response: &'a Response,
1468        output_schema: &Schema<'_>,
1469        cfg: &'a ConfigBag,
1470    ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
1471        let body = response.body().bytes().unwrap_or(&[]);
1472        Ok(http_output_deserializer(
1473            self.codec.create_deserializer(body),
1474            response,
1475            output_schema,
1476            cfg,
1477        ))
1478    }
1479
1480    /// Returns a deserializer for a modeled error response.
1481    ///
1482    /// Overridden rather than left to the default forwarding, which would hand the error to
1483    /// [`deserialize_response`](Self::deserialize_response) and therefore to a success-mode
1484    /// wrapper. A success-mode wrapper gives an empty body to the codec, but services do send
1485    /// empty error bodies — an S3 `HEAD` is the standard case — and those responses must still
1486    /// populate their header and status members. The error helper tolerates that while reading
1487    /// the same bindings.
1488    ///
1489    /// A protocol with an error envelope (restXml, awsQuery) overrides this again to position
1490    /// the body inside the envelope before wrapping.
1491    fn deserialize_error_response<'a>(
1492        &self,
1493        response: &'a Response,
1494        cfg: &'a ConfigBag,
1495    ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
1496        let body = response.body().bytes().unwrap_or(&[]);
1497        Ok(http_error_deserializer(
1498            self.codec.create_deserializer(body),
1499            response,
1500            cfg,
1501        ))
1502    }
1503
1504    fn payload_codec(&self) -> Option<&dyn crate::codec::DynCodec> {
1505        Some(&self.codec)
1506    }
1507
1508    fn update_endpoint(
1509        &self,
1510        request: &mut Request,
1511        endpoint: &aws_smithy_types::endpoint::Endpoint,
1512        cfg: &ConfigBag,
1513    ) -> Result<(), SerdeError> {
1514        apply_http_endpoint(request, endpoint, cfg)
1515    }
1516}
1517
1518#[cfg(test)]
1519mod tests {
1520    use super::*;
1521    use crate::serde::SerializableStruct;
1522    use crate::{prelude::*, ShapeType};
1523
1524    #[test]
1525    fn visited_members_inline_dedup() {
1526        let mut v = VisitedMembers::new();
1527        // First insert returns true; identical second insert returns false.
1528        assert!(v.insert(3));
1529        assert!(!v.insert(3));
1530        // Distinct indices return true.
1531        assert!(v.insert(7));
1532        assert!(v.insert(0));
1533        // Re-asserting any of the above returns false.
1534        assert!(!v.insert(3));
1535        assert!(!v.insert(7));
1536        assert!(!v.insert(0));
1537    }
1538
1539    #[test]
1540    fn visited_members_spills_to_overflow() {
1541        // Exceeding INLINE_CAPACITY pushes additional entries into the heap
1542        // overflow vec. Both halves must dedup correctly.
1543        let mut v = VisitedMembers::new();
1544        let n = VisitedMembers::INLINE_CAPACITY;
1545        // Fill inline storage.
1546        for i in 0..n {
1547            assert!(v.insert(i), "fresh inline insert at {i} must return true");
1548        }
1549        // Inserting again into inline range returns false (no allocation).
1550        for i in 0..n {
1551            assert!(
1552                !v.insert(i),
1553                "duplicate inline insert at {i} must return false"
1554            );
1555        }
1556        // Cross the capacity boundary — these go into overflow.
1557        assert!(v.insert(n));
1558        assert!(v.insert(n + 5));
1559        // Duplicates of overflow entries return false.
1560        assert!(!v.insert(n));
1561        assert!(!v.insert(n + 5));
1562        // And duplicates of inline entries still return false even after
1563        // the overflow vec is non-empty.
1564        assert!(!v.insert(0));
1565    }
1566
1567    struct TestSerializer {
1568        output: Vec<u8>,
1569    }
1570
1571    impl FinishSerializer for TestSerializer {
1572        fn finish(self) -> Vec<u8> {
1573            self.output
1574        }
1575    }
1576
1577    impl ShapeSerializer for TestSerializer {
1578        fn write_struct(
1579            &mut self,
1580            _: &Schema<'_>,
1581            value: &dyn SerializableStruct,
1582        ) -> Result<(), SerdeError> {
1583            self.output.push(b'{');
1584            value.serialize_members(self)?;
1585            self.output.push(b'}');
1586            Ok(())
1587        }
1588        fn write_list(
1589            &mut self,
1590            _: &Schema<'_>,
1591            _: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
1592        ) -> Result<(), SerdeError> {
1593            Ok(())
1594        }
1595        fn write_map(
1596            &mut self,
1597            _: &Schema<'_>,
1598            _: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
1599        ) -> Result<(), SerdeError> {
1600            Ok(())
1601        }
1602        fn write_boolean(&mut self, _: &Schema<'_>, _: bool) -> Result<(), SerdeError> {
1603            Ok(())
1604        }
1605        fn write_byte(&mut self, _: &Schema<'_>, _: i8) -> Result<(), SerdeError> {
1606            Ok(())
1607        }
1608        fn write_short(&mut self, _: &Schema<'_>, _: i16) -> Result<(), SerdeError> {
1609            Ok(())
1610        }
1611        fn write_integer(&mut self, _: &Schema<'_>, _: i32) -> Result<(), SerdeError> {
1612            Ok(())
1613        }
1614        fn write_long(&mut self, _: &Schema<'_>, _: i64) -> Result<(), SerdeError> {
1615            Ok(())
1616        }
1617        fn write_float(&mut self, _: &Schema<'_>, _: f32) -> Result<(), SerdeError> {
1618            Ok(())
1619        }
1620        fn write_double(&mut self, _: &Schema<'_>, _: f64) -> Result<(), SerdeError> {
1621            Ok(())
1622        }
1623        fn write_big_integer(
1624            &mut self,
1625            _: &Schema<'_>,
1626            _: &aws_smithy_types::BigInteger,
1627        ) -> Result<(), SerdeError> {
1628            Ok(())
1629        }
1630        fn write_big_decimal(
1631            &mut self,
1632            _: &Schema<'_>,
1633            _: &aws_smithy_types::BigDecimal,
1634        ) -> Result<(), SerdeError> {
1635            Ok(())
1636        }
1637        fn write_string(&mut self, _: &Schema<'_>, v: &str) -> Result<(), SerdeError> {
1638            self.output.extend_from_slice(v.as_bytes());
1639            Ok(())
1640        }
1641        fn write_blob(
1642            &mut self,
1643            _: &Schema<'_>,
1644            _: aws_smithy_types::Blob,
1645        ) -> Result<(), SerdeError> {
1646            Ok(())
1647        }
1648        fn write_timestamp(
1649            &mut self,
1650            _: &Schema<'_>,
1651            _: &aws_smithy_types::DateTime,
1652        ) -> Result<(), SerdeError> {
1653            Ok(())
1654        }
1655        fn write_document(
1656            &mut self,
1657            _: &Schema<'_>,
1658            _: &aws_smithy_types::Document,
1659        ) -> Result<(), SerdeError> {
1660            Ok(())
1661        }
1662        fn write_null(&mut self, _: &Schema<'_>) -> Result<(), SerdeError> {
1663            Ok(())
1664        }
1665    }
1666
1667    struct TestDeserializer<'a> {
1668        input: &'a [u8],
1669    }
1670
1671    impl ShapeDeserializer for TestDeserializer<'_> {
1672        /// Rejects an empty body, which is what the real JSON and XML codecs do. Without that,
1673        /// "was the body handed to the codec?" is unobservable and the error path's empty-body
1674        /// tolerance cannot be told apart from the success path.
1675        fn read_struct(
1676            &mut self,
1677            _: &Schema<'_>,
1678            _: &mut dyn FnMut(&Schema<'_>, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
1679        ) -> Result<(), SerdeError> {
1680            if self.input.is_empty() {
1681                return Err(SerdeError::invalid_input("expected a body, found none"));
1682            }
1683            Ok(())
1684        }
1685        fn read_list(
1686            &mut self,
1687            _: &Schema<'_>,
1688            _: &mut dyn FnMut(&mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
1689        ) -> Result<(), SerdeError> {
1690            Ok(())
1691        }
1692        fn read_map(
1693            &mut self,
1694            _: &Schema<'_>,
1695            _: &mut dyn FnMut(String, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
1696        ) -> Result<(), SerdeError> {
1697            Ok(())
1698        }
1699        fn read_boolean(&mut self, _: &Schema<'_>) -> Result<bool, SerdeError> {
1700            Ok(false)
1701        }
1702        fn read_byte(&mut self, _: &Schema<'_>) -> Result<i8, SerdeError> {
1703            Ok(0)
1704        }
1705        fn read_short(&mut self, _: &Schema<'_>) -> Result<i16, SerdeError> {
1706            Ok(0)
1707        }
1708        fn read_integer(&mut self, _: &Schema<'_>) -> Result<i32, SerdeError> {
1709            Ok(0)
1710        }
1711        fn read_long(&mut self, _: &Schema<'_>) -> Result<i64, SerdeError> {
1712            Ok(0)
1713        }
1714        fn read_float(&mut self, _: &Schema<'_>) -> Result<f32, SerdeError> {
1715            Ok(0.0)
1716        }
1717        fn read_double(&mut self, _: &Schema<'_>) -> Result<f64, SerdeError> {
1718            Ok(0.0)
1719        }
1720        fn read_big_integer(
1721            &mut self,
1722            _: &Schema<'_>,
1723        ) -> Result<aws_smithy_types::BigInteger, SerdeError> {
1724            use std::str::FromStr;
1725            Ok(aws_smithy_types::BigInteger::from_str("0").unwrap())
1726        }
1727        fn read_big_decimal(
1728            &mut self,
1729            _: &Schema<'_>,
1730        ) -> Result<aws_smithy_types::BigDecimal, SerdeError> {
1731            use std::str::FromStr;
1732            Ok(aws_smithy_types::BigDecimal::from_str("0").unwrap())
1733        }
1734        fn read_string(&mut self, _: &Schema<'_>) -> Result<String, SerdeError> {
1735            Ok(String::from_utf8_lossy(self.input).into_owned())
1736        }
1737        fn read_blob(&mut self, _: &Schema<'_>) -> Result<aws_smithy_types::Blob, SerdeError> {
1738            Ok(aws_smithy_types::Blob::new(vec![]))
1739        }
1740        fn read_timestamp(
1741            &mut self,
1742            _: &Schema<'_>,
1743        ) -> Result<aws_smithy_types::DateTime, SerdeError> {
1744            Ok(aws_smithy_types::DateTime::from_secs(0))
1745        }
1746        fn read_document(
1747            &mut self,
1748            _: &Schema<'_>,
1749        ) -> Result<aws_smithy_types::Document, SerdeError> {
1750            Ok(aws_smithy_types::Document::Null)
1751        }
1752        fn is_null(&self) -> bool {
1753            false
1754        }
1755        fn container_size(&self) -> Option<usize> {
1756            None
1757        }
1758    }
1759
1760    #[derive(Debug)]
1761    struct TestCodec;
1762
1763    impl Codec for TestCodec {
1764        type Serializer = TestSerializer;
1765        type Deserializer<'a> = TestDeserializer<'a>;
1766        fn create_serializer(&self) -> Self::Serializer {
1767            TestSerializer { output: Vec::new() }
1768        }
1769        fn create_deserializer<'a>(&self, input: &'a [u8]) -> Self::Deserializer<'a> {
1770            TestDeserializer { input }
1771        }
1772    }
1773
1774    static TEST_SCHEMA: Schema<'static> =
1775        Schema::new(crate::shape_id!("test", "TestStruct"), ShapeType::Structure);
1776
1777    struct EmptyStruct;
1778    impl SerializableStruct for EmptyStruct {
1779        fn schema(&self) -> &Schema<'_> {
1780            &TEST_SCHEMA
1781        }
1782
1783        fn serialize_members(&self, _: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1784            Ok(())
1785        }
1786    }
1787
1788    static NAME_MEMBER: Schema<'static> = Schema::new_member(
1789        crate::shape_id!("test", "TestStruct"),
1790        ShapeType::String,
1791        "name",
1792        0,
1793    );
1794    static MEMBERS: &[&Schema<'_>] = &[&NAME_MEMBER];
1795    static STRUCT_WITH_MEMBER: Schema<'static> = Schema::new_struct(
1796        crate::shape_id!("test", "TestStruct"),
1797        ShapeType::Structure,
1798        MEMBERS,
1799    );
1800
1801    struct NameStruct;
1802    impl SerializableStruct for NameStruct {
1803        fn schema(&self) -> &Schema<'_> {
1804            &STRUCT_WITH_MEMBER
1805        }
1806
1807        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1808            s.write_string(&NAME_MEMBER, "Alice")
1809        }
1810    }
1811
1812    fn make_protocol() -> HttpBindingProtocol<TestCodec> {
1813        HttpBindingProtocol::new(
1814            crate::shape_id!("test", "proto"),
1815            TestCodec,
1816            "application/test",
1817        )
1818    }
1819
1820    /// A REST protocol resolves its route from the operation's `@http` template, so a path that
1821    /// codegen computed for a *different* protocol must not be prefixed onto it.
1822    ///
1823    /// This is the mirror image of the assertion `AwsJsonRpcProtocol` and `AwsQueryProtocol` make
1824    /// about their own fixed routes: whichever layer owns the route ignores a foreign one. It is
1825    /// reachable only through `Config::builder().protocol(..)` — generated REST clients pass `""`,
1826    /// so the request would otherwise be routed to the concatenation of both protocols' paths, e.g.
1827    /// `/service/Svc/operation/GetStats/stats` on an rpcv2Cbor-generated client.
1828    #[test]
1829    fn serialize_request_ignores_a_route_computed_for_another_protocol() {
1830        static HTTP_SCHEMA: Schema<'static> = Schema::new_struct(
1831            crate::shape_id!("test", "GetStatsRequest"),
1832            ShapeType::Structure,
1833            &[],
1834        )
1835        .with_http(crate::traits::HttpTrait::new("PUT", "/stats", None));
1836
1837        for foreign_route in [
1838            // An rpcv2Cbor route, as codegen emits for that protocol.
1839            "/service/Svc/operation/GetStats",
1840            // awsJson's and awsQuery's fixed route.
1841            "/",
1842        ] {
1843            let request = make_protocol()
1844                .serialize_request(
1845                    &EmptyStruct,
1846                    &HTTP_SCHEMA,
1847                    foreign_route,
1848                    &ConfigBag::base(),
1849                )
1850                .unwrap();
1851            assert_eq!(
1852                "/stats",
1853                request.uri(),
1854                "the `@http` template is authoritative; the route {foreign_route} was computed for \
1855                 another protocol and must be ignored",
1856            );
1857            assert_eq!("PUT", request.method());
1858        }
1859    }
1860
1861    /// The counterpart guard: with no `@http` trait there is no template, so the endpoint *is* the
1862    /// template and is still honored — including label expansion. Several tests below rely on this.
1863    #[test]
1864    fn serialize_request_uses_endpoint_as_template_without_an_http_trait() {
1865        let request = make_protocol()
1866            .serialize_request(&EmptyStruct, &TEST_SCHEMA, "/some/path", &ConfigBag::base())
1867            .unwrap();
1868        assert_eq!("/some/path", request.uri());
1869    }
1870
1871    #[test]
1872    fn serialize_sets_content_type() {
1873        // A struct with body members gets Content-Type
1874        let request = make_protocol()
1875            .serialize_request(
1876                &EmptyStruct,
1877                &STRUCT_WITH_MEMBER,
1878                "https://example.com",
1879                &ConfigBag::base(),
1880            )
1881            .unwrap();
1882        assert_eq!(
1883            request.headers().get("Content-Type").unwrap(),
1884            "application/test"
1885        );
1886    }
1887
1888    #[test]
1889    fn serialize_no_body_members_omits_content_type() {
1890        // A struct with no members gets no Content-Type per REST-JSON spec
1891        let request = make_protocol()
1892            .serialize_request(
1893                &EmptyStruct,
1894                &TEST_SCHEMA,
1895                "https://example.com",
1896                &ConfigBag::base(),
1897            )
1898            .unwrap();
1899        assert!(request.headers().get("Content-Type").is_none());
1900    }
1901
1902    /// The bug fix at the center of PR #4686 review: when a presigning
1903    /// interceptor (or any other caller) stores a `SharedHeaderOmitSettings`
1904    /// in the config bag, the runtime must not insert protocol-default
1905    /// Content-Type or Content-Length headers — even on the standard-body
1906    /// path used by ordinary structure inputs.
1907    #[test]
1908    fn presigning_omit_settings_suppress_default_content_headers() {
1909        use crate::header_omit_settings::{HeaderOmitSettings, SharedHeaderOmitSettings};
1910        use aws_smithy_types::config_bag::Layer;
1911
1912        #[derive(Debug)]
1913        struct OmitBoth;
1914        impl HeaderOmitSettings for OmitBoth {
1915            fn should_omit_default_content_type(&self) -> bool {
1916                true
1917            }
1918            fn should_omit_default_content_length(&self) -> bool {
1919                true
1920            }
1921        }
1922
1923        let mut layer = Layer::new("test_omit");
1924        layer.store_put(SharedHeaderOmitSettings::new(OmitBoth));
1925        let cfg = ConfigBag::of_layers(vec![layer]);
1926
1927        let request = make_protocol()
1928            .serialize_request(
1929                &NameStruct,
1930                &STRUCT_WITH_MEMBER,
1931                "https://example.com",
1932                &cfg,
1933            )
1934            .unwrap();
1935        assert!(
1936            request.headers().get("Content-Type").is_none(),
1937            "presigning omit suppresses default Content-Type"
1938        );
1939        assert!(
1940            request.headers().get("Content-Length").is_none(),
1941            "presigning omit suppresses default Content-Length"
1942        );
1943    }
1944
1945    /// Companion to `presigning_omit_settings_suppress_default_content_headers`:
1946    /// when no `SharedHeaderOmitSettings` is in the config bag, the runtime
1947    /// inserts the protocol's default Content-Type plus a body-length-derived
1948    /// Content-Length. `NameStruct` writes a single `"Alice"` member, which
1949    /// the test codec frames as `{Alice}` for a 7-byte body — exercises the
1950    /// `len > 0` branch of the Content-Length insertion logic.
1951    #[test]
1952    fn default_content_headers_inserted_when_omit_settings_absent() {
1953        let request = make_protocol()
1954            .serialize_request(
1955                &NameStruct,
1956                &STRUCT_WITH_MEMBER,
1957                "https://example.com",
1958                &ConfigBag::base(),
1959            )
1960            .unwrap();
1961        assert_eq!(
1962            request
1963                .headers()
1964                .get("Content-Type")
1965                .expect("Content-Type set"),
1966            "application/test"
1967        );
1968        assert_eq!(
1969            request
1970                .headers()
1971                .get("Content-Length")
1972                .expect("Content-Length set"),
1973            "7"
1974        );
1975    }
1976
1977    /// When a schema is annotated `with_no_body_members()`, the body codec
1978    /// must not be invoked (no XML/JSON wrapper element gets opened, no
1979    /// `serialize_members` re-entry through a Proxy). HTTP-bound members
1980    /// still get routed to headers/query/labels via the binder. Verified by
1981    /// substituting a body codec that panics on any write call.
1982    #[test]
1983    fn serialize_skips_body_codec_when_no_body_members() {
1984        use std::sync::atomic::{AtomicUsize, Ordering};
1985
1986        static WRITE_CALLS: AtomicUsize = AtomicUsize::new(0);
1987
1988        struct PanicSerializer;
1989        impl FinishSerializer for PanicSerializer {
1990            fn finish(self) -> Vec<u8> {
1991                panic!("body codec finish() called — short-circuit failed");
1992            }
1993        }
1994        impl ShapeSerializer for PanicSerializer {
1995            fn write_struct(
1996                &mut self,
1997                _: &Schema<'_>,
1998                _: &dyn SerializableStruct,
1999            ) -> Result<(), SerdeError> {
2000                WRITE_CALLS.fetch_add(1, Ordering::SeqCst);
2001                panic!("body codec write_struct() called — short-circuit failed");
2002            }
2003            fn write_list(
2004                &mut self,
2005                _: &Schema<'_>,
2006                _: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
2007            ) -> Result<(), SerdeError> {
2008                panic!("body codec write_list() called");
2009            }
2010            fn write_map(
2011                &mut self,
2012                _: &Schema<'_>,
2013                _: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
2014            ) -> Result<(), SerdeError> {
2015                panic!("body codec write_map() called");
2016            }
2017            fn write_boolean(&mut self, _: &Schema<'_>, _: bool) -> Result<(), SerdeError> {
2018                panic!("body codec write_boolean() called");
2019            }
2020            fn write_byte(&mut self, _: &Schema<'_>, _: i8) -> Result<(), SerdeError> {
2021                panic!("body codec write_byte() called");
2022            }
2023            fn write_short(&mut self, _: &Schema<'_>, _: i16) -> Result<(), SerdeError> {
2024                panic!("body codec write_short() called");
2025            }
2026            fn write_integer(&mut self, _: &Schema<'_>, _: i32) -> Result<(), SerdeError> {
2027                panic!("body codec write_integer() called");
2028            }
2029            fn write_long(&mut self, _: &Schema<'_>, _: i64) -> Result<(), SerdeError> {
2030                panic!("body codec write_long() called");
2031            }
2032            fn write_float(&mut self, _: &Schema<'_>, _: f32) -> Result<(), SerdeError> {
2033                panic!("body codec write_float() called");
2034            }
2035            fn write_double(&mut self, _: &Schema<'_>, _: f64) -> Result<(), SerdeError> {
2036                panic!("body codec write_double() called");
2037            }
2038            fn write_big_integer(
2039                &mut self,
2040                _: &Schema<'_>,
2041                _: &aws_smithy_types::BigInteger,
2042            ) -> Result<(), SerdeError> {
2043                panic!("body codec write_big_integer() called");
2044            }
2045            fn write_big_decimal(
2046                &mut self,
2047                _: &Schema<'_>,
2048                _: &aws_smithy_types::BigDecimal,
2049            ) -> Result<(), SerdeError> {
2050                panic!("body codec write_big_decimal() called");
2051            }
2052            fn write_string(&mut self, _: &Schema<'_>, _: &str) -> Result<(), SerdeError> {
2053                panic!("body codec write_string() called");
2054            }
2055            fn write_blob(
2056                &mut self,
2057                _: &Schema<'_>,
2058                _: aws_smithy_types::Blob,
2059            ) -> Result<(), SerdeError> {
2060                panic!("body codec write_blob() called");
2061            }
2062            fn write_timestamp(
2063                &mut self,
2064                _: &Schema<'_>,
2065                _: &aws_smithy_types::DateTime,
2066            ) -> Result<(), SerdeError> {
2067                panic!("body codec write_timestamp() called");
2068            }
2069            fn write_document(
2070                &mut self,
2071                _: &Schema<'_>,
2072                _: &aws_smithy_types::Document,
2073            ) -> Result<(), SerdeError> {
2074                panic!("body codec write_document() called");
2075            }
2076            fn write_null(&mut self, _: &Schema<'_>) -> Result<(), SerdeError> {
2077                panic!("body codec write_null() called");
2078            }
2079        }
2080
2081        #[derive(Debug)]
2082        struct PanicCodec;
2083        impl Codec for PanicCodec {
2084            type Serializer = PanicSerializer;
2085            type Deserializer<'a> = TestDeserializer<'a>;
2086            fn create_serializer(&self) -> Self::Serializer {
2087                PanicSerializer
2088            }
2089            fn create_deserializer<'a>(&self, input: &'a [u8]) -> Self::Deserializer<'a> {
2090                TestDeserializer { input }
2091            }
2092        }
2093
2094        // Header-only struct: one `@httpHeader` member, marked
2095        // `with_no_body_members()`. The runtime should never touch the body
2096        // codec.
2097        static HEADER_MEMBER: Schema<'static> = Schema::new_member(
2098            crate::shape_id!("test", "HeaderOnlyStruct"),
2099            ShapeType::String,
2100            "x_header",
2101            0,
2102        )
2103        .with_http_header("X-Header");
2104        static HEADER_MEMBERS: &[&Schema<'_>] = &[&HEADER_MEMBER];
2105        static HEADER_ONLY_SCHEMA: Schema<'static> = Schema::new_struct(
2106            crate::shape_id!("test", "HeaderOnlyStruct"),
2107            ShapeType::Structure,
2108            HEADER_MEMBERS,
2109        )
2110        .with_no_body_members();
2111
2112        struct HeaderOnlyStruct;
2113        impl SerializableStruct for HeaderOnlyStruct {
2114            fn schema(&self) -> &Schema<'_> {
2115                &HEADER_ONLY_SCHEMA
2116            }
2117
2118            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
2119                s.write_string(&HEADER_MEMBER, "hello")
2120            }
2121        }
2122
2123        let protocol = HttpBindingProtocol::new(
2124            crate::shape_id!("test", "testProtocol"),
2125            PanicCodec,
2126            "application/test",
2127        );
2128        let request = protocol
2129            .serialize_request(
2130                &HeaderOnlyStruct,
2131                &HEADER_ONLY_SCHEMA,
2132                "https://example.com",
2133                &ConfigBag::base(),
2134            )
2135            .unwrap();
2136
2137        // No body, no Content-Type, header is set
2138        assert_eq!(request.body().bytes().unwrap_or(&[]), b"");
2139        assert!(request.headers().get("Content-Type").is_none());
2140        assert_eq!(request.headers().get("X-Header").unwrap(), "hello");
2141        // Sanity: the panic-on-write codec was never written to
2142        assert_eq!(WRITE_CALLS.load(Ordering::SeqCst), 0);
2143    }
2144
2145    // -- PayloadHint must agree with the scan it replaces -----------------------------------------
2146    //
2147    // `PayloadHint` is an optimization, so the only property worth pinning is
2148    // that a recorded hint and the derived scan produce byte-identical requests.
2149    // The negative control below proves these are not vacuous by showing that a
2150    // *wrong* hint does change the body — i.e. the hint really is consulted.
2151
2152    static STRUCT_PAYLOAD_MEMBER: Schema<'static> = Schema::new_member(
2153        crate::shape_id!("test", "StructPayloadStruct"),
2154        ShapeType::Structure,
2155        "body",
2156        0,
2157    )
2158    .with_http_payload();
2159    static STRUCT_PAYLOAD_MEMBERS: &[&Schema<'_>] = &[&STRUCT_PAYLOAD_MEMBER];
2160
2161    /// No hint recorded — the runtime derives `has_struct_payload` by scanning.
2162    static STRUCT_PAYLOAD_UNKNOWN: Schema<'static> = Schema::new_struct(
2163        crate::shape_id!("test", "StructPayloadStruct"),
2164        ShapeType::Structure,
2165        STRUCT_PAYLOAD_MEMBERS,
2166    );
2167    /// Same schema, correct hint recorded.
2168    static STRUCT_PAYLOAD_HINTED: Schema<'static> = Schema::new_struct(
2169        crate::shape_id!("test", "StructPayloadStruct"),
2170        ShapeType::Structure,
2171        STRUCT_PAYLOAD_MEMBERS,
2172    )
2173    .with_payload_hint(crate::PayloadHint::StructPayload);
2174    /// Same schema, deliberately *wrong* hint — negative control only.
2175    static STRUCT_PAYLOAD_MIS_HINTED: Schema<'static> = Schema::new_struct(
2176        crate::shape_id!("test", "StructPayloadStruct"),
2177        ShapeType::Structure,
2178        STRUCT_PAYLOAD_MEMBERS,
2179    )
2180    .with_payload_hint(crate::PayloadHint::NoStructPayload);
2181
2182    /// Writes an `@httpPayload` struct member. `TestSerializer::write_struct`
2183    /// emits `{` .. `}`, so the framing is directly observable: one pair of
2184    /// braces means only the payload member framed the body, two means the
2185    /// codec wrapped it as well.
2186    struct StructPayloadStruct;
2187    impl SerializableStruct for StructPayloadStruct {
2188        fn schema(&self) -> &Schema<'_> {
2189            &STRUCT_PAYLOAD_UNKNOWN
2190        }
2191
2192        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
2193            s.write_struct(&STRUCT_PAYLOAD_MEMBER, &NameStruct)
2194        }
2195    }
2196
2197    fn body_for(schema: &Schema<'_>, input: &dyn SerializableStruct) -> Vec<u8> {
2198        make_protocol()
2199            .serialize_request(input, schema, "https://example.com", &ConfigBag::base())
2200            .unwrap()
2201            .body()
2202            .bytes()
2203            .unwrap_or(&[])
2204            .to_vec()
2205    }
2206
2207    #[test]
2208    fn struct_payload_hint_agrees_with_derived_scan() {
2209        let derived = body_for(&STRUCT_PAYLOAD_UNKNOWN, &StructPayloadStruct);
2210        let hinted = body_for(&STRUCT_PAYLOAD_HINTED, &StructPayloadStruct);
2211
2212        // Only the payload member frames the body.
2213        assert_eq!(derived, b"{Alice}");
2214        assert_eq!(hinted, derived);
2215    }
2216
2217    #[test]
2218    fn no_struct_payload_hint_agrees_with_derived_scan() {
2219        static HINTED: Schema<'static> = Schema::new_struct(
2220            crate::shape_id!("test", "TestStruct"),
2221            ShapeType::Structure,
2222            MEMBERS,
2223        )
2224        .with_payload_hint(crate::PayloadHint::NoStructPayload);
2225
2226        let derived = body_for(&STRUCT_WITH_MEMBER, &NameStruct);
2227        let hinted = body_for(&HINTED, &NameStruct);
2228
2229        // Codec supplies the framing.
2230        assert_eq!(derived, b"{Alice}");
2231        assert_eq!(hinted, derived);
2232    }
2233
2234    /// Negative control. Proves the hint is actually consulted, so the two
2235    /// agreement tests above are not passing trivially.
2236    ///
2237    /// The observable effect is stronger than lost framing: `has_struct_payload`
2238    /// feeds *two* decisions — whether the codec adds top-level framing, and
2239    /// (at the `has_body_members` computation) whether the serialized body is
2240    /// kept at all. A member carrying `@httpPayload` is excluded from the
2241    /// "unbound member" scan, so once the hint wrongly says there is no struct
2242    /// payload, nothing marks the struct as having body members and the payload
2243    /// is discarded. This is why the hint's default has to mean "unknown"
2244    /// rather than either boolean — a wrong value in *either* direction
2245    /// corrupts the request.
2246    #[test]
2247    fn wrong_struct_payload_hint_changes_request() {
2248        assert_eq!(
2249            body_for(&STRUCT_PAYLOAD_MIS_HINTED, &StructPayloadStruct),
2250            b"",
2251            "a mis-recorded hint must be observable, otherwise the agreement \
2252             tests above prove nothing"
2253        );
2254    }
2255
2256    #[test]
2257    fn payload_hint_defaults_to_unknown() {
2258        assert_eq!(
2259            STRUCT_PAYLOAD_UNKNOWN.payload_hint(),
2260            crate::PayloadHint::Unknown
2261        );
2262        assert_eq!(TEST_SCHEMA.payload_hint(), crate::PayloadHint::Unknown);
2263    }
2264
2265    /// Inverse case: a struct WITHOUT `with_no_body_members()` (default
2266    /// `has_body_members == true`) and an actual body member must still
2267    /// invoke the body codec. Guards against accidentally short-circuiting
2268    /// schemas that were never opted in.
2269    #[test]
2270    fn serialize_invokes_body_codec_when_has_body_members() {
2271        let request = make_protocol()
2272            .serialize_request(
2273                &NameStruct,
2274                &STRUCT_WITH_MEMBER,
2275                "https://example.com",
2276                &ConfigBag::base(),
2277            )
2278            .unwrap();
2279        // TestSerializer writes "{Alice}" — the wrapper braces prove the
2280        // body codec was invoked.
2281        assert_eq!(request.body().bytes().unwrap(), b"{Alice}");
2282        assert_eq!(
2283            request.headers().get("Content-Type").unwrap(),
2284            "application/test"
2285        );
2286    }
2287
2288    #[test]
2289    fn serialize_sets_uri() {
2290        let request = make_protocol()
2291            .serialize_request(
2292                &EmptyStruct,
2293                &TEST_SCHEMA,
2294                "https://example.com/path",
2295                &ConfigBag::base(),
2296            )
2297            .unwrap();
2298        assert_eq!(request.uri(), "https://example.com/path");
2299    }
2300
2301    #[test]
2302    fn serialize_body() {
2303        let request = make_protocol()
2304            .serialize_request(
2305                &NameStruct,
2306                &STRUCT_WITH_MEMBER,
2307                "https://example.com",
2308                &ConfigBag::base(),
2309            )
2310            .unwrap();
2311        assert_eq!(request.body().bytes().unwrap(), b"{Alice}");
2312    }
2313
2314    // -- @httpPayload must not borrow from the caller's stack ------------------------------------
2315
2316    static BLOB_PAYLOAD_MEMBER: Schema<'static> = Schema::new_member(
2317        crate::shape_id!("test", "PayloadStruct"),
2318        ShapeType::Blob,
2319        "data",
2320        0,
2321    )
2322    .with_http_payload();
2323    static BLOB_PAYLOAD_MEMBERS: &[&Schema<'_>] = &[&BLOB_PAYLOAD_MEMBER];
2324    static BLOB_PAYLOAD_STRUCT: Schema<'static> = Schema::new_struct(
2325        crate::shape_id!("test", "PayloadStruct"),
2326        ShapeType::Structure,
2327        BLOB_PAYLOAD_MEMBERS,
2328    );
2329
2330    static STRING_PAYLOAD_MEMBER: Schema<'static> = Schema::new_member(
2331        crate::shape_id!("test", "PayloadStruct"),
2332        ShapeType::String,
2333        "data",
2334        0,
2335    )
2336    .with_http_payload();
2337    static STRING_PAYLOAD_MEMBERS: &[&Schema<'_>] = &[&STRING_PAYLOAD_MEMBER];
2338    static STRING_PAYLOAD_STRUCT: Schema<'static> = Schema::new_struct(
2339        crate::shape_id!("test", "PayloadStruct"),
2340        ShapeType::Structure,
2341        STRING_PAYLOAD_MEMBERS,
2342    );
2343
2344    /// Computes its payload into a local that is dropped before `serialize_members` returns.
2345    ///
2346    /// This is legal against the public `ShapeSerializer` contract: neither `write_blob` nor
2347    /// `write_string` requires its argument to outlive the serializer. Generated code happens to
2348    /// pass values derived from the input struct, but a hand-written `SerializableStruct` — the
2349    /// case the dynamic-client and type-registry work exists to enable — need not.
2350    struct LocallyComputedPayload {
2351        blob: bool,
2352    }
2353
2354    impl SerializableStruct for LocallyComputedPayload {
2355        fn schema(&self) -> &Schema<'_> {
2356            &STRING_PAYLOAD_STRUCT
2357        }
2358
2359        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
2360            if self.blob {
2361                // Heap-allocated here and freed at the end of this scope.
2362                let computed: Vec<u8> = (0u8..64).collect();
2363                s.write_blob(&BLOB_PAYLOAD_MEMBER, aws_smithy_types::Blob::new(computed))
2364            } else {
2365                let computed: String = (0..16).map(|i| char::from(b'a' + (i % 26) as u8)).collect();
2366                s.write_string(&STRING_PAYLOAD_MEMBER, &computed)
2367            }
2368        }
2369    }
2370
2371    /// Regression test for a use-after-free in the `@httpPayload` binding.
2372    ///
2373    /// `raw_payload` used to be `Option<&'a [u8]>`, populated by transmuting the `write_blob` /
2374    /// `write_string` argument's anonymous lifetime up to `'a`. `'a` is bound by the input schema
2375    /// and the headers, *not* by the serialized value, so the assertion was unfounded: a payload
2376    /// computed into a local was freed before `serialize_request` read `raw_payload` back. Miri
2377    /// reported `encountered a dangling reference (use-after-free)` while constructing the
2378    /// `Option<&[u8]>`.
2379    ///
2380    /// Both members are now owned, so the payload outlives the frame that produced it. Run this
2381    /// under Miri — a plain `cargo test` may well pass on freed-but-unreused memory:
2382    ///
2383    /// ```text
2384    /// cargo +nightly miri test -p aws-smithy-schema --lib http_payload_from_a_local
2385    /// ```
2386    #[test]
2387    fn http_payload_from_a_local_does_not_dangle() {
2388        let request = make_protocol()
2389            .serialize_request(
2390                &LocallyComputedPayload { blob: true },
2391                &BLOB_PAYLOAD_STRUCT,
2392                "https://example.com",
2393                &ConfigBag::base(),
2394            )
2395            .unwrap();
2396        // Reading the bytes is the point: it dereferences what `raw_payload` retained.
2397        let expected: Vec<u8> = (0u8..64).collect();
2398        assert_eq!(request.body().bytes().unwrap(), &expected[..]);
2399
2400        let request = make_protocol()
2401            .serialize_request(
2402                &LocallyComputedPayload { blob: false },
2403                &STRING_PAYLOAD_STRUCT,
2404                "https://example.com",
2405                &ConfigBag::base(),
2406            )
2407            .unwrap();
2408        assert_eq!(request.body().bytes().unwrap(), b"abcdefghijklmnop");
2409    }
2410
2411    /// The blob payload path must not copy: `write_blob` takes an owned `Blob`, so the `Bytes`
2412    /// handed to `SdkBody` should be the very same allocation the caller built.
2413    ///
2414    /// Asserting on the pointer is the only way to observe this; a byte-equality assertion passes
2415    /// either way and would not notice a regression back to copying.
2416    #[test]
2417    fn blob_payload_reaches_the_body_without_copying() {
2418        struct OwnedBlobPayload(aws_smithy_types::Blob);
2419        impl SerializableStruct for OwnedBlobPayload {
2420            fn schema(&self) -> &Schema<'_> {
2421                &BLOB_PAYLOAD_STRUCT
2422            }
2423
2424            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
2425                s.write_blob(&BLOB_PAYLOAD_MEMBER, self.0.clone())
2426            }
2427        }
2428
2429        let payload = aws_smithy_types::Blob::new(vec![7u8; 4096]);
2430        let src_ptr = payload.as_ref().as_ptr();
2431
2432        let request = make_protocol()
2433            .serialize_request(
2434                &OwnedBlobPayload(payload),
2435                &BLOB_PAYLOAD_STRUCT,
2436                "https://example.com",
2437                &ConfigBag::base(),
2438            )
2439            .unwrap();
2440
2441        let body = request.body().bytes().expect("payload body is in memory");
2442        assert_eq!(body.len(), 4096);
2443        assert_eq!(
2444            body.as_ptr(),
2445            src_ptr,
2446            "blob payload was copied; `write_blob` should move the `Bytes` handle all the way \
2447             into `SdkBody`"
2448        );
2449    }
2450
2451    #[test]
2452    fn deserialize_response() {
2453        let response = Response::new(
2454            200u16.try_into().unwrap(),
2455            SdkBody::from(r#"{"name":"Bob"}"#),
2456        );
2457        let base_cfg = ConfigBag::base();
2458        let mut deser = make_protocol()
2459            .deserialize_response(&response, &TEST_SCHEMA, &base_cfg)
2460            .unwrap();
2461        assert_eq!(deser.read_string(&STRING).unwrap(), r#"{"name":"Bob"}"#);
2462    }
2463
2464    #[test]
2465    fn update_endpoint() {
2466        let mut request = make_protocol()
2467            .serialize_request(
2468                &EmptyStruct,
2469                &TEST_SCHEMA,
2470                "https://old.example.com",
2471                &ConfigBag::base(),
2472            )
2473            .unwrap();
2474        let endpoint = aws_smithy_types::endpoint::Endpoint::builder()
2475            .url("https://new.example.com")
2476            .build();
2477        make_protocol()
2478            .update_endpoint(&mut request, &endpoint, &ConfigBag::base())
2479            .unwrap();
2480        assert_eq!(request.uri(), "https://new.example.com/");
2481    }
2482
2483    #[test]
2484    fn protocol_id() {
2485        let protocol = HttpBindingProtocol::new(
2486            crate::shape_id!("aws.protocols", "restJson1"),
2487            TestCodec,
2488            "application/json",
2489        );
2490        assert_eq!(protocol.protocol_id().as_str(), "aws.protocols#restJson1");
2491    }
2492
2493    #[test]
2494    fn invalid_uri_returns_error() {
2495        assert!(make_protocol()
2496            .serialize_request(
2497                &EmptyStruct,
2498                &TEST_SCHEMA,
2499                "not a valid uri\n\n",
2500                &ConfigBag::base()
2501            )
2502            .is_err());
2503    }
2504
2505    // -- @httpHeader tests --
2506
2507    static HEADER_MEMBER: Schema<'static> = Schema::new_member(
2508        crate::shape_id!("test", "S"),
2509        ShapeType::String,
2510        "xToken",
2511        0,
2512    )
2513    .with_http_header("X-Token");
2514
2515    static HEADER_SCHEMA: Schema<'static> = Schema::new_struct(
2516        crate::shape_id!("test", "S"),
2517        ShapeType::Structure,
2518        &[&HEADER_MEMBER],
2519    );
2520
2521    struct HeaderStruct;
2522    impl SerializableStruct for HeaderStruct {
2523        fn schema(&self) -> &Schema<'_> {
2524            &HEADER_SCHEMA
2525        }
2526
2527        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
2528            s.write_string(&HEADER_MEMBER, "my-token-value")
2529        }
2530    }
2531
2532    #[test]
2533    fn http_header_string() {
2534        let request = make_protocol()
2535            .serialize_request(
2536                &HeaderStruct,
2537                &HEADER_SCHEMA,
2538                "https://example.com",
2539                &ConfigBag::base(),
2540            )
2541            .unwrap();
2542        assert_eq!(request.headers().get("X-Token").unwrap(), "my-token-value");
2543    }
2544
2545    /// String list headers are quoted with the rule the generated (non-schema) header
2546    /// serializer uses: on `,`, `"`, `(`, `)` and surrounding whitespace.
2547    #[test]
2548    fn http_header_string_list_quoting_matches_legacy() {
2549        static LIST_MEMBER: Schema<'static> =
2550            Schema::new_member(crate::shape_id!("test", "L"), ShapeType::List, "items", 0)
2551                .with_http_header("X-Items");
2552        static LIST_SCHEMA: Schema<'static> = Schema::new_struct(
2553            crate::shape_id!("test", "L"),
2554            ShapeType::Structure,
2555            &[&LIST_MEMBER],
2556        );
2557        struct ListStruct;
2558        impl SerializableStruct for ListStruct {
2559            fn schema(&self) -> &Schema<'_> {
2560                &LIST_SCHEMA
2561            }
2562            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
2563                s.write_list(&LIST_MEMBER, &|e| {
2564                    for v in ["plain", " a", "b ", "(x)", "c,d", "q\"q"] {
2565                        e.write_string(&crate::prelude::STRING, v)?;
2566                    }
2567                    Ok(())
2568                })
2569            }
2570        }
2571        let request = make_protocol()
2572            .serialize_request(
2573                &ListStruct,
2574                &LIST_SCHEMA,
2575                "https://example.com",
2576                &ConfigBag::base(),
2577            )
2578            .unwrap();
2579        assert_eq!(
2580            request.headers().get("X-Items").unwrap(),
2581            r#"plain, " a", "b ", "(x)", "c,d", "q\"q""#
2582        );
2583    }
2584
2585    /// Header binding for a schema built at *runtime*: the structural strings
2586    /// are borrowed from a local arena (so `'a` is a function-body lifetime,
2587    /// not `'static`), while the header name itself is `'static` — standing in
2588    /// for a name a dynamic client would intern once at model-load time.
2589    ///
2590    /// This is what the `@httpHeader` constructor pin costs and what it still
2591    /// permits: a non-`'static` schema binds headers fine, and its cached
2592    /// `HeaderName` is freed with the schema.
2593    #[test]
2594    fn http_header_on_a_runtime_built_schema() {
2595        // Structural strings: owned locally, dropped at end of scope. The
2596        // header name is interned, which is how a real runtime-built schema
2597        // satisfies the `'static` bound on `with_http_header`.
2598        let arena: Vec<String> = vec![
2599            String::from("token"),
2600            String::from("runtime-value"),
2601            String::from("X-Interned-Token"),
2602        ];
2603
2604        let member: Schema<'_> = Schema::new_member(
2605            crate::shape_id!("test", "S"),
2606            ShapeType::String,
2607            &arena[0],
2608            0,
2609        )
2610        .with_http_header(crate::intern_header_name(&arena[2]));
2611
2612        // The binder's fast path is available for this runtime schema.
2613        assert_eq!(
2614            member.http_header().unwrap().value_static(),
2615            Some("X-Interned-Token")
2616        );
2617
2618        let members = [&member];
2619        let schema = Schema::new_struct(
2620            crate::shape_id!("test", "S"),
2621            ShapeType::Structure,
2622            &members,
2623        );
2624
2625        struct RuntimeStruct<'a> {
2626            schema: &'a Schema<'a>,
2627            member: &'a Schema<'a>,
2628            value: &'a str,
2629        }
2630        impl SerializableStruct for RuntimeStruct<'_> {
2631            fn schema(&self) -> &Schema<'_> {
2632                self.schema
2633            }
2634
2635            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
2636                s.write_string(self.member, self.value)
2637            }
2638        }
2639        let input = RuntimeStruct {
2640            schema: &schema,
2641            member: &member,
2642            value: &arena[1],
2643        };
2644        assert!(std::ptr::eq(input.schema(), &schema));
2645
2646        let request = make_protocol()
2647            .serialize_request(&input, &schema, "https://example.com", &ConfigBag::base())
2648            .unwrap();
2649
2650        assert_eq!(
2651            request.headers().get("X-Interned-Token").unwrap(),
2652            "runtime-value"
2653        );
2654    }
2655
2656    static INT_HEADER_MEMBER: Schema<'static> = Schema::new_member(
2657        crate::shape_id!("test", "S"),
2658        ShapeType::Integer,
2659        "retryCount",
2660        0,
2661    )
2662    .with_http_header("X-Retry-Count");
2663
2664    static INT_HEADER_SCHEMA: Schema<'static> = Schema::new_struct(
2665        crate::shape_id!("test", "S"),
2666        ShapeType::Structure,
2667        &[&INT_HEADER_MEMBER],
2668    );
2669
2670    struct IntHeaderStruct;
2671    impl SerializableStruct for IntHeaderStruct {
2672        fn schema(&self) -> &Schema<'_> {
2673            &INT_HEADER_SCHEMA
2674        }
2675
2676        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
2677            s.write_integer(&INT_HEADER_MEMBER, 3)
2678        }
2679    }
2680
2681    #[test]
2682    fn http_header_integer() {
2683        let request = make_protocol()
2684            .serialize_request(
2685                &IntHeaderStruct,
2686                &INT_HEADER_SCHEMA,
2687                "https://example.com",
2688                &ConfigBag::base(),
2689            )
2690            .unwrap();
2691        assert_eq!(request.headers().get("X-Retry-Count").unwrap(), "3");
2692    }
2693
2694    static BOOL_HEADER_MEMBER: Schema<'static> = Schema::new_member(
2695        crate::shape_id!("test", "S"),
2696        ShapeType::Boolean,
2697        "verbose",
2698        0,
2699    )
2700    .with_http_header("X-Verbose");
2701
2702    static BOOL_HEADER_SCHEMA: Schema<'static> = Schema::new_struct(
2703        crate::shape_id!("test", "S"),
2704        ShapeType::Structure,
2705        &[&BOOL_HEADER_MEMBER],
2706    );
2707
2708    struct BoolHeaderStruct;
2709    impl SerializableStruct for BoolHeaderStruct {
2710        fn schema(&self) -> &Schema<'_> {
2711            &BOOL_HEADER_SCHEMA
2712        }
2713
2714        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
2715            s.write_boolean(&BOOL_HEADER_MEMBER, true)
2716        }
2717    }
2718
2719    #[test]
2720    fn http_header_boolean() {
2721        let request = make_protocol()
2722            .serialize_request(
2723                &BoolHeaderStruct,
2724                &BOOL_HEADER_SCHEMA,
2725                "https://example.com",
2726                &ConfigBag::base(),
2727            )
2728            .unwrap();
2729        assert_eq!(request.headers().get("X-Verbose").unwrap(), "true");
2730    }
2731
2732    // -- @httpQuery tests --
2733
2734    static QUERY_MEMBER: Schema<'static> =
2735        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "color", 0)
2736            .with_http_query("color");
2737
2738    static QUERY_SCHEMA: Schema<'static> = Schema::new_struct(
2739        crate::shape_id!("test", "S"),
2740        ShapeType::Structure,
2741        &[&QUERY_MEMBER],
2742    );
2743
2744    struct QueryStruct;
2745    impl SerializableStruct for QueryStruct {
2746        fn schema(&self) -> &Schema<'_> {
2747            &QUERY_SCHEMA
2748        }
2749
2750        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
2751            s.write_string(&QUERY_MEMBER, "blue")
2752        }
2753    }
2754
2755    #[test]
2756    fn http_query_string() {
2757        let request = make_protocol()
2758            .serialize_request(
2759                &QueryStruct,
2760                &QUERY_SCHEMA,
2761                "https://example.com/things",
2762                &ConfigBag::base(),
2763            )
2764            .unwrap();
2765        assert_eq!(request.uri(), "https://example.com/things?color=blue");
2766    }
2767
2768    static INT_QUERY_MEMBER: Schema<'static> =
2769        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::Integer, "size", 0)
2770            .with_http_query("size");
2771
2772    static INT_QUERY_SCHEMA: Schema<'static> = Schema::new_struct(
2773        crate::shape_id!("test", "S"),
2774        ShapeType::Structure,
2775        &[&INT_QUERY_MEMBER],
2776    );
2777
2778    struct IntQueryStruct;
2779    impl SerializableStruct for IntQueryStruct {
2780        fn schema(&self) -> &Schema<'_> {
2781            &INT_QUERY_SCHEMA
2782        }
2783
2784        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
2785            s.write_integer(&INT_QUERY_MEMBER, 42)
2786        }
2787    }
2788
2789    #[test]
2790    fn http_query_integer() {
2791        let request = make_protocol()
2792            .serialize_request(
2793                &IntQueryStruct,
2794                &INT_QUERY_SCHEMA,
2795                "https://example.com/things",
2796                &ConfigBag::base(),
2797            )
2798            .unwrap();
2799        assert_eq!(request.uri(), "https://example.com/things?size=42");
2800    }
2801
2802    // -- Multiple @httpQuery params --
2803
2804    static Q1: Schema<'static> =
2805        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "a", 0)
2806            .with_http_query("a");
2807    static Q2: Schema<'static> =
2808        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "b", 1)
2809            .with_http_query("b");
2810    static MULTI_QUERY_SCHEMA: Schema<'static> = Schema::new_struct(
2811        crate::shape_id!("test", "S"),
2812        ShapeType::Structure,
2813        &[&Q1, &Q2],
2814    );
2815
2816    struct MultiQueryStruct;
2817    impl SerializableStruct for MultiQueryStruct {
2818        fn schema(&self) -> &Schema<'_> {
2819            &MULTI_QUERY_SCHEMA
2820        }
2821
2822        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
2823            s.write_string(&Q1, "x")?;
2824            s.write_string(&Q2, "y")
2825        }
2826    }
2827
2828    #[test]
2829    fn http_query_multiple_params() {
2830        let request = make_protocol()
2831            .serialize_request(
2832                &MultiQueryStruct,
2833                &MULTI_QUERY_SCHEMA,
2834                "https://example.com",
2835                &ConfigBag::base(),
2836            )
2837            .unwrap();
2838        assert_eq!(request.uri(), "https://example.com?a=x&b=y");
2839    }
2840
2841    // -- @httpQuery with percent-encoding --
2842
2843    #[test]
2844    fn http_query_percent_encodes_values() {
2845        struct SpaceQueryStruct;
2846        impl SerializableStruct for SpaceQueryStruct {
2847            fn schema(&self) -> &Schema<'_> {
2848                &QUERY_SCHEMA
2849            }
2850
2851            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
2852                s.write_string(&QUERY_MEMBER, "hello world")
2853            }
2854        }
2855        let request = make_protocol()
2856            .serialize_request(
2857                &SpaceQueryStruct,
2858                &QUERY_SCHEMA,
2859                "https://example.com",
2860                &ConfigBag::base(),
2861            )
2862            .unwrap();
2863        assert_eq!(request.uri(), "https://example.com?color=hello%20world");
2864    }
2865
2866    // -- @httpLabel tests --
2867
2868    static LABEL_MEMBER: Schema<'static> = Schema::new_member(
2869        crate::shape_id!("test", "S"),
2870        ShapeType::String,
2871        "bucketName",
2872        0,
2873    )
2874    .with_http_label();
2875
2876    static LABEL_SCHEMA: Schema<'static> = Schema::new_struct(
2877        crate::shape_id!("test", "S"),
2878        ShapeType::Structure,
2879        &[&LABEL_MEMBER],
2880    );
2881
2882    struct LabelStruct;
2883    impl SerializableStruct for LabelStruct {
2884        fn schema(&self) -> &Schema<'_> {
2885            &LABEL_SCHEMA
2886        }
2887
2888        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
2889            s.write_string(&LABEL_MEMBER, "my-bucket")
2890        }
2891    }
2892
2893    #[test]
2894    fn http_label_substitution() {
2895        let request = make_protocol()
2896            .serialize_request(
2897                &LabelStruct,
2898                &LABEL_SCHEMA,
2899                "https://example.com/{bucketName}/objects",
2900                &ConfigBag::base(),
2901            )
2902            .unwrap();
2903        assert_eq!(request.uri(), "https://example.com/my-bucket/objects");
2904    }
2905
2906    #[test]
2907    fn http_label_percent_encodes() {
2908        struct SpecialLabelStruct;
2909        impl SerializableStruct for SpecialLabelStruct {
2910            fn schema(&self) -> &Schema<'_> {
2911                &LABEL_SCHEMA
2912            }
2913
2914            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
2915                s.write_string(&LABEL_MEMBER, "my bucket/name")
2916            }
2917        }
2918        let request = make_protocol()
2919            .serialize_request(
2920                &SpecialLabelStruct,
2921                &LABEL_SCHEMA,
2922                "https://example.com/{bucketName}",
2923                &ConfigBag::base(),
2924            )
2925            .unwrap();
2926        assert!(request.uri().contains("my%20bucket%2Fname"));
2927    }
2928
2929    static INT_LABEL_MEMBER: Schema<'static> = Schema::new_member(
2930        crate::shape_id!("test", "S"),
2931        ShapeType::Integer,
2932        "itemId",
2933        0,
2934    )
2935    .with_http_label();
2936
2937    static INT_LABEL_SCHEMA: Schema<'static> = Schema::new_struct(
2938        crate::shape_id!("test", "S"),
2939        ShapeType::Structure,
2940        &[&INT_LABEL_MEMBER],
2941    );
2942
2943    struct IntLabelStruct;
2944    impl SerializableStruct for IntLabelStruct {
2945        fn schema(&self) -> &Schema<'_> {
2946            &INT_LABEL_SCHEMA
2947        }
2948
2949        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
2950            s.write_integer(&INT_LABEL_MEMBER, 123)
2951        }
2952    }
2953
2954    #[test]
2955    fn http_label_integer() {
2956        let request = make_protocol()
2957            .serialize_request(
2958                &IntLabelStruct,
2959                &INT_LABEL_SCHEMA,
2960                "https://example.com/items/{itemId}",
2961                &ConfigBag::base(),
2962            )
2963            .unwrap();
2964        assert_eq!(request.uri(), "https://example.com/items/123");
2965    }
2966
2967    /// From review of PR #4871: a missing or empty label used to be dropped, collapsing its
2968    /// path segment (`/{Bucket}/{Key+}` with an empty bucket became `//my-key`). It is an
2969    /// error now, worded as in the generated (non-schema) serializer.
2970    #[test]
2971    fn missing_or_empty_label_is_an_error() {
2972        fn expand(template: &str, labels: &[(&'static str, &str)]) -> Result<String, String> {
2973            let labels: Vec<(Cow<'_, str>, String)> = labels
2974                .iter()
2975                .map(|(n, v)| (Cow::Borrowed(*n), v.to_string()))
2976                .collect();
2977            let mut out = String::new();
2978            append_uri_with_labels(template, &labels, &mut out)
2979                .map(|()| out)
2980                .map_err(|e| e.to_string())
2981        }
2982        let template = "/{Bucket}/{Key+}";
2983        assert_eq!(
2984            expand(template, &[("Bucket", "b"), ("Key", "a//b")]).unwrap(),
2985            "/b/a//b",
2986            "an empty inner segment of a greedy label is still allowed"
2987        );
2988        for (labels, missing) in [
2989            (&[("Key", "k")][..], "Bucket"),
2990            (&[("Bucket", ""), ("Key", "k")][..], "Bucket"),
2991            (&[("Bucket", "b")][..], "Key"),
2992            (&[("Bucket", "b"), ("Key", "")][..], "Key"),
2993        ] {
2994            let err = expand(template, labels).expect_err(&format!("{labels:?}"));
2995            assert!(
2996                err.contains(&format!("{missing} was missing: cannot be empty or unset")),
2997                "{err}"
2998            );
2999        }
3000        // An unmatched `{` is unrelated and still passed through verbatim.
3001        assert_eq!(expand("/a{b", &[]).unwrap(), "/a{b");
3002    }
3003
3004    #[test]
3005    fn empty_label_fails_serialize_request() {
3006        static BUCKET: Schema<'static> = Schema::new_member(
3007            crate::shape_id!("test", "Get"),
3008            ShapeType::String,
3009            "Bucket",
3010            0,
3011        )
3012        .with_http_label();
3013        static KEY: Schema<'static> =
3014            Schema::new_member(crate::shape_id!("test", "Get"), ShapeType::String, "Key", 1)
3015                .with_http_label();
3016        static GET_SCHEMA: Schema<'static> = Schema::new_struct(
3017            crate::shape_id!("test", "Get"),
3018            ShapeType::Structure,
3019            &[&BUCKET, &KEY],
3020        )
3021        .with_http(crate::traits::HttpTrait::new(
3022            "GET",
3023            "/{Bucket}/{Key+}",
3024            None,
3025        ));
3026        struct Get(&'static str);
3027        impl SerializableStruct for Get {
3028            fn schema(&self) -> &Schema<'_> {
3029                &GET_SCHEMA
3030            }
3031            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
3032                s.write_string(&BUCKET, self.0)?;
3033                s.write_string(&KEY, "my-key")
3034            }
3035        }
3036        let protocol = make_protocol();
3037        let request = protocol
3038            .serialize_request(&Get("b"), &GET_SCHEMA, "", &ConfigBag::base())
3039            .unwrap();
3040        assert_eq!(request.uri(), "/b/my-key");
3041        let err = protocol
3042            .serialize_request(&Get(""), &GET_SCHEMA, "", &ConfigBag::base())
3043            .expect_err("an empty bucket must not produce `//my-key`");
3044        assert!(
3045            err.to_string()
3046                .contains("Bucket was missing: cannot be empty or unset"),
3047            "{err}"
3048        );
3049    }
3050
3051    // -- Combined: @httpHeader + @httpQuery + @httpLabel + body --
3052
3053    static COMBINED_LABEL: Schema<'static> =
3054        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "id", 0)
3055            .with_http_label();
3056    static COMBINED_HEADER: Schema<'static> =
3057        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "token", 1)
3058            .with_http_header("X-Token");
3059    static COMBINED_QUERY: Schema<'static> = Schema::new_member(
3060        crate::shape_id!("test", "S"),
3061        ShapeType::String,
3062        "filter",
3063        2,
3064    )
3065    .with_http_query("filter");
3066    static COMBINED_BODY: Schema<'static> =
3067        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::String, "data", 3);
3068    static COMBINED_SCHEMA: Schema<'static> = Schema::new_struct(
3069        crate::shape_id!("test", "S"),
3070        ShapeType::Structure,
3071        &[
3072            &COMBINED_LABEL,
3073            &COMBINED_HEADER,
3074            &COMBINED_QUERY,
3075            &COMBINED_BODY,
3076        ],
3077    );
3078
3079    struct CombinedStruct;
3080    impl SerializableStruct for CombinedStruct {
3081        fn schema(&self) -> &Schema<'_> {
3082            &COMBINED_SCHEMA
3083        }
3084
3085        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
3086            s.write_string(&COMBINED_LABEL, "item-42")?;
3087            s.write_string(&COMBINED_HEADER, "secret")?;
3088            s.write_string(&COMBINED_QUERY, "active")?;
3089            s.write_string(&COMBINED_BODY, "payload-data")
3090        }
3091    }
3092
3093    #[test]
3094    fn combined_bindings() {
3095        let request = make_protocol()
3096            .serialize_request(
3097                &CombinedStruct,
3098                &COMBINED_SCHEMA,
3099                "https://example.com/{id}/details",
3100                &ConfigBag::base(),
3101            )
3102            .unwrap();
3103        assert_eq!(
3104            request.uri(),
3105            "https://example.com/item-42/details?filter=active"
3106        );
3107        // Header
3108        assert_eq!(request.headers().get("X-Token").unwrap(), "secret");
3109        // Body contains only the unbound member
3110        let body = request.body().bytes().unwrap();
3111        assert!(body
3112            .windows(b"payload-data".len())
3113            .any(|w| w == b"payload-data"));
3114    }
3115
3116    // -- @httpPrefixHeaders tests --
3117
3118    static PREFIX_MEMBER: Schema<'static> =
3119        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::Map, "metadata", 0)
3120            .with_http_prefix_headers("X-Meta-");
3121
3122    static PREFIX_SCHEMA: Schema<'static> = Schema::new_struct(
3123        crate::shape_id!("test", "S"),
3124        ShapeType::Structure,
3125        &[&PREFIX_MEMBER],
3126    );
3127
3128    struct PrefixHeaderStruct;
3129    impl SerializableStruct for PrefixHeaderStruct {
3130        fn schema(&self) -> &Schema<'_> {
3131            &PREFIX_SCHEMA
3132        }
3133
3134        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
3135            s.write_map(&PREFIX_MEMBER, &|s| {
3136                s.write_string(&STRING, "Color")?;
3137                s.write_string(&STRING, "red")?;
3138                s.write_string(&STRING, "Size")?;
3139                s.write_string(&STRING, "large")?;
3140                Ok(())
3141            })
3142        }
3143    }
3144
3145    #[test]
3146    fn http_prefix_headers() {
3147        let request = make_protocol()
3148            .serialize_request(
3149                &PrefixHeaderStruct,
3150                &PREFIX_SCHEMA,
3151                "https://example.com",
3152                &ConfigBag::base(),
3153            )
3154            .unwrap();
3155        assert_eq!(request.headers().get("X-Meta-Color").unwrap(), "red");
3156        assert_eq!(request.headers().get("X-Meta-Size").unwrap(), "large");
3157    }
3158
3159    /// Serializes a single prefix-header entry; the map key becomes part of the header name.
3160    fn serialize_prefix_entry(key: &'static str, value: &'static str) -> Result<(), SerdeError> {
3161        struct Entry(&'static str, &'static str);
3162        impl SerializableStruct for Entry {
3163            fn schema(&self) -> &Schema<'_> {
3164                &PREFIX_SCHEMA
3165            }
3166            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
3167                s.write_map(&PREFIX_MEMBER, &|s| {
3168                    s.write_string(&STRING, self.0)?;
3169                    s.write_string(&STRING, self.1)
3170                })
3171            }
3172        }
3173        make_protocol()
3174            .serialize_request(
3175                &Entry(key, value),
3176                &PREFIX_SCHEMA,
3177                "https://example.com",
3178                &ConfigBag::base(),
3179            )
3180            .map(|_| ())
3181    }
3182
3183    /// From review of PR #4871: an invalid header name or value used to panic inside
3184    /// `Headers::insert`. It must be an error that names the member and the prefix or header,
3185    /// but not the value.
3186    #[test]
3187    fn invalid_prefix_header_name_or_value_is_an_error_not_a_panic() {
3188        for (key, value) in [
3189            ("has space", "secret-1"),
3190            ("colon:", "secret-2"),
3191            ("non-ascii-é", "secret-3"),
3192            ("Ok", "secret\nbreak"),
3193        ] {
3194            let err = serialize_prefix_entry(key, value)
3195                .expect_err(&format!("{key:?}/{value:?} must be rejected"));
3196            let message = err.to_string();
3197            assert!(
3198                message.contains("metadata") && message.contains("prefix `X-Meta-`"),
3199                "{message}"
3200            );
3201            assert!(!message.contains(value), "value leaked: {message}");
3202        }
3203        serialize_prefix_entry("Ok", "fine").expect("a valid entry still serializes");
3204    }
3205
3206    #[test]
3207    fn invalid_header_value_is_an_error_not_a_panic() {
3208        struct Scalar;
3209        impl SerializableStruct for Scalar {
3210            fn schema(&self) -> &Schema<'_> {
3211                &HEADER_SCHEMA
3212            }
3213            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
3214                s.write_string(&HEADER_MEMBER, "secret\r\ninjected: 1")
3215            }
3216        }
3217        static LIST_MEMBER: Schema<'static> =
3218            Schema::new_member(crate::shape_id!("test", "L"), ShapeType::List, "items", 0)
3219                .with_http_header("X-Items");
3220        static LIST_SCHEMA: Schema<'static> = Schema::new_struct(
3221            crate::shape_id!("test", "L"),
3222            ShapeType::Structure,
3223            &[&LIST_MEMBER],
3224        );
3225        struct List;
3226        impl SerializableStruct for List {
3227            fn schema(&self) -> &Schema<'_> {
3228                &LIST_SCHEMA
3229            }
3230            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
3231                s.write_list(&LIST_MEMBER, &|e| {
3232                    e.write_string(&STRING, "ok")?;
3233                    e.write_string(&STRING, "secret\nvalue")
3234                })
3235            }
3236        }
3237        let protocol = make_protocol();
3238        let cases: [(&dyn SerializableStruct, &Schema<'static>, &str); 2] = [
3239            (&Scalar, &HEADER_SCHEMA, "xToken to header `X-Token`"),
3240            (&List, &LIST_SCHEMA, "items to header `X-Items`"),
3241        ];
3242        for (input, schema, expected) in cases {
3243            let err = protocol
3244                .serialize_request(input, schema, "https://example.com", &ConfigBag::base())
3245                .expect_err("an invalid header value must be rejected");
3246            let message = err.to_string();
3247            assert!(message.contains(expected), "{message}");
3248            assert!(!message.contains("secret"), "value leaked: {message}");
3249        }
3250    }
3251
3252    /// A modeled header name that `http` rejects has no cached `HeaderName`, so the insert
3253    /// falls back to the string path, which reports the error. It must do so on every
3254    /// request, not only the one that first computed the cache entry.
3255    #[test]
3256    fn invalid_modeled_header_name_is_an_error_not_a_panic() {
3257        static BAD_MEMBER: Schema<'static> =
3258            Schema::new_member(crate::shape_id!("test", "B"), ShapeType::String, "bad", 0)
3259                .with_http_header("has space");
3260        static BAD_SCHEMA: Schema<'static> = Schema::new_struct(
3261            crate::shape_id!("test", "B"),
3262            ShapeType::Structure,
3263            &[&BAD_MEMBER],
3264        );
3265        struct Bad;
3266        impl SerializableStruct for Bad {
3267            fn schema(&self) -> &Schema<'_> {
3268                &BAD_SCHEMA
3269            }
3270            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
3271                s.write_string(&BAD_MEMBER, "value")
3272            }
3273        }
3274        let protocol = make_protocol();
3275        for _ in 0..2 {
3276            let err = protocol
3277                .serialize_request(&Bad, &BAD_SCHEMA, "https://example.com", &ConfigBag::base())
3278                .expect_err("an invalid header name must be rejected");
3279            let message = err.to_string();
3280            assert!(message.contains("bad to header `has space`"), "{message}");
3281        }
3282    }
3283
3284    /// Scalar, list and blob headers all insert the member's cached `HeaderName`. A second
3285    /// request reuses the cached names and must produce the same headers.
3286    #[test]
3287    fn cached_header_names_are_reused_by_every_header_write() {
3288        static SCALAR: Schema<'static> = Schema::new_member(
3289            crate::shape_id!("test", "H"),
3290            ShapeType::String,
3291            "scalar",
3292            0,
3293        )
3294        .with_http_header("X-Amz-Scalar");
3295        static LIST: Schema<'static> =
3296            Schema::new_member(crate::shape_id!("test", "H"), ShapeType::List, "list", 1)
3297                .with_http_header("x-amz-list");
3298        static BLOB: Schema<'static> =
3299            Schema::new_member(crate::shape_id!("test", "H"), ShapeType::Blob, "blob", 2)
3300                .with_http_header("Content-MD5");
3301        static SCHEMA: Schema<'static> = Schema::new_struct(
3302            crate::shape_id!("test", "H"),
3303            ShapeType::Structure,
3304            &[&SCALAR, &LIST, &BLOB],
3305        );
3306        struct Headers3;
3307        impl SerializableStruct for Headers3 {
3308            fn schema(&self) -> &Schema<'_> {
3309                &SCHEMA
3310            }
3311            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
3312                s.write_string(&SCALAR, "one")?;
3313                s.write_list(&LIST, &|e| {
3314                    e.write_string(&STRING, "a")?;
3315                    e.write_string(&STRING, "b")
3316                })?;
3317                s.write_blob(&BLOB, aws_smithy_types::Blob::new("md5"))
3318            }
3319        }
3320        let protocol = make_protocol();
3321        for _ in 0..2 {
3322            let request = protocol
3323                .serialize_request(
3324                    &Headers3,
3325                    &SCHEMA,
3326                    "https://example.com",
3327                    &ConfigBag::base(),
3328                )
3329                .unwrap();
3330            let mut headers: Vec<_> = request
3331                .headers()
3332                .iter()
3333                .filter(|(name, _)| !name.starts_with("content-type"))
3334                .collect();
3335            headers.sort();
3336            assert_eq!(
3337                headers,
3338                [
3339                    ("content-md5", "bWQ1"),
3340                    ("x-amz-list", "a, b"),
3341                    ("x-amz-scalar", "one"),
3342                ]
3343            );
3344        }
3345    }
3346
3347    // -- @httpQueryParams tests --
3348
3349    static QUERY_PARAMS_MEMBER: Schema<'static> =
3350        Schema::new_member(crate::shape_id!("test", "S"), ShapeType::Map, "params", 0)
3351            .with_http_query_params();
3352
3353    static QUERY_PARAMS_SCHEMA: Schema<'static> = Schema::new_struct(
3354        crate::shape_id!("test", "S"),
3355        ShapeType::Structure,
3356        &[&QUERY_PARAMS_MEMBER],
3357    );
3358
3359    struct QueryParamsStruct;
3360    impl SerializableStruct for QueryParamsStruct {
3361        fn schema(&self) -> &Schema<'_> {
3362            &QUERY_PARAMS_SCHEMA
3363        }
3364
3365        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
3366            s.write_map(&QUERY_PARAMS_MEMBER, &|s| {
3367                s.write_string(&STRING, "page")?;
3368                s.write_string(&STRING, "2")?;
3369                s.write_string(&STRING, "limit")?;
3370                s.write_string(&STRING, "50")?;
3371                Ok(())
3372            })
3373        }
3374    }
3375
3376    #[test]
3377    fn http_query_params() {
3378        let request = make_protocol()
3379            .serialize_request(
3380                &QueryParamsStruct,
3381                &QUERY_PARAMS_SCHEMA,
3382                "https://example.com",
3383                &ConfigBag::base(),
3384            )
3385            .unwrap();
3386        assert_eq!(request.uri(), "https://example.com?page=2&limit=50");
3387    }
3388
3389    // -- Timestamp in header defaults to http-date --
3390
3391    static TS_HEADER_MEMBER: Schema<'static> = Schema::new_member(
3392        crate::shape_id!("test", "S"),
3393        ShapeType::Timestamp,
3394        "ifModified",
3395        0,
3396    )
3397    .with_http_header("If-Modified-Since");
3398
3399    static TS_HEADER_SCHEMA: Schema<'static> = Schema::new_struct(
3400        crate::shape_id!("test", "S"),
3401        ShapeType::Structure,
3402        &[&TS_HEADER_MEMBER],
3403    );
3404
3405    struct TimestampHeaderStruct;
3406    impl SerializableStruct for TimestampHeaderStruct {
3407        fn schema(&self) -> &Schema<'_> {
3408            &TS_HEADER_SCHEMA
3409        }
3410
3411        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
3412            s.write_timestamp(&TS_HEADER_MEMBER, &aws_smithy_types::DateTime::from_secs(0))
3413        }
3414    }
3415
3416    #[test]
3417    fn timestamp_header_uses_http_date() {
3418        let request = make_protocol()
3419            .serialize_request(
3420                &TimestampHeaderStruct,
3421                &TS_HEADER_SCHEMA,
3422                "https://example.com",
3423                &ConfigBag::base(),
3424            )
3425            .unwrap();
3426        let value = request.headers().get("If-Modified-Since").unwrap();
3427        // http-date format: "Thu, 01 Jan 1970 00:00:00 GMT"
3428        assert!(value.contains("1970"), "expected http-date, got: {value}");
3429    }
3430
3431    // -- Timestamp in query defaults to date-time --
3432
3433    static TS_QUERY_MEMBER: Schema<'static> = Schema::new_member(
3434        crate::shape_id!("test", "S"),
3435        ShapeType::Timestamp,
3436        "since",
3437        0,
3438    )
3439    .with_http_query("since");
3440
3441    static TS_QUERY_SCHEMA: Schema<'static> = Schema::new_struct(
3442        crate::shape_id!("test", "S"),
3443        ShapeType::Structure,
3444        &[&TS_QUERY_MEMBER],
3445    );
3446
3447    struct TimestampQueryStruct;
3448    impl SerializableStruct for TimestampQueryStruct {
3449        fn schema(&self) -> &Schema<'_> {
3450            &TS_QUERY_SCHEMA
3451        }
3452
3453        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
3454            s.write_timestamp(&TS_QUERY_MEMBER, &aws_smithy_types::DateTime::from_secs(0))
3455        }
3456    }
3457
3458    #[test]
3459    fn timestamp_query_uses_date_time() {
3460        let request = make_protocol()
3461            .serialize_request(
3462                &TimestampQueryStruct,
3463                &TS_QUERY_SCHEMA,
3464                "https://example.com",
3465                &ConfigBag::base(),
3466            )
3467            .unwrap();
3468        assert_eq!(
3469            request.uri(),
3470            "https://example.com?since=1970-01-01T00%3A00%3A00Z"
3471        );
3472    }
3473
3474    // -- Unbound members go to body, bound members do not --
3475
3476    static BOUND_MEMBER: Schema<'static> = Schema::new_member(
3477        crate::shape_id!("test", "S"),
3478        ShapeType::String,
3479        "headerVal",
3480        0,
3481    )
3482    .with_http_header("X-Val");
3483    static UNBOUND_MEMBER: Schema<'static> = Schema::new_member(
3484        crate::shape_id!("test", "S"),
3485        ShapeType::String,
3486        "bodyVal",
3487        1,
3488    );
3489    static MIXED_SCHEMA: Schema<'static> = Schema::new_struct(
3490        crate::shape_id!("test", "S"),
3491        ShapeType::Structure,
3492        &[&BOUND_MEMBER, &UNBOUND_MEMBER],
3493    );
3494
3495    struct MixedStruct;
3496    impl SerializableStruct for MixedStruct {
3497        fn schema(&self) -> &Schema<'_> {
3498            &MIXED_SCHEMA
3499        }
3500
3501        fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
3502            s.write_string(&BOUND_MEMBER, "in-header")?;
3503            s.write_string(&UNBOUND_MEMBER, "in-body")
3504        }
3505    }
3506
3507    #[test]
3508    fn bound_members_not_in_body() {
3509        let request = make_protocol()
3510            .serialize_request(
3511                &MixedStruct,
3512                &MIXED_SCHEMA,
3513                "https://example.com",
3514                &ConfigBag::base(),
3515            )
3516            .unwrap();
3517        let body = std::str::from_utf8(request.body().bytes().unwrap()).unwrap();
3518        assert!(
3519            body.contains("in-body"),
3520            "body should contain unbound member"
3521        );
3522        assert!(
3523            !body.contains("in-header"),
3524            "body should NOT contain header-bound member"
3525        );
3526        assert_eq!(request.headers().get("X-Val").unwrap(), "in-header");
3527    }
3528
3529    // ---------------------------------------------------------------------------------
3530    // response bindings are owned by the protocol
3531    //
3532    // A populated header member is only reachable through the response wrapper, so these are
3533    // the assertions that the wiring exists at all. The wrapper's own branch decisions are
3534    // covered in `super::response`.
3535    // ---------------------------------------------------------------------------------
3536
3537    static RESP_NAME: Schema<'static> = Schema::new_member(
3538        crate::shape_id!("test", "Out"),
3539        ShapeType::String,
3540        "name",
3541        0,
3542    )
3543    .with_http_header("x-name");
3544    static RESP_STATUS: Schema<'static> = Schema::new_member(
3545        crate::shape_id!("test", "Out"),
3546        ShapeType::Integer,
3547        "status",
3548        1,
3549    )
3550    .with_http_response_code();
3551    static RESP_BOUND: Schema<'static> = Schema::new_struct(
3552        crate::shape_id!("test", "Out"),
3553        ShapeType::Structure,
3554        &[&RESP_NAME, &RESP_STATUS],
3555    );
3556
3557    static RESP_BODY_FIELD: Schema<'static> = Schema::new_member(
3558        crate::shape_id!("test", "Out"),
3559        ShapeType::String,
3560        "note",
3561        2,
3562    );
3563    /// Bound *and* body members, so the body codec is invoked and its empty-input strictness is
3564    /// reachable.
3565    static RESP_MIXED: Schema<'static> = Schema::new_struct(
3566        crate::shape_id!("test", "Out"),
3567        ShapeType::Structure,
3568        &[&RESP_NAME, &RESP_STATUS, &RESP_BODY_FIELD],
3569    );
3570
3571    static RESP_STREAM: Schema<'static> = Schema::new_member(
3572        crate::shape_id!("test", "Stream"),
3573        ShapeType::Blob,
3574        "body",
3575        0,
3576    )
3577    .with_http_payload()
3578    .with_streaming();
3579    static RESP_STREAM_HEADER: Schema<'static> = Schema::new_member(
3580        crate::shape_id!("test", "Stream"),
3581        ShapeType::String,
3582        "name",
3583        1,
3584    )
3585    .with_http_header("x-name");
3586    static RESP_STREAMING_OUT: Schema<'static> = Schema::new_struct(
3587        crate::shape_id!("test", "Stream"),
3588        ShapeType::Structure,
3589        &[&RESP_STREAM, &RESP_STREAM_HEADER],
3590    );
3591
3592    fn response_with(
3593        status: u16,
3594        headers: &[(&str, &str)],
3595        body: aws_smithy_types::body::SdkBody,
3596    ) -> Response {
3597        let mut builder = http::Response::builder().status(status);
3598        for (name, value) in headers {
3599            builder = builder.header(*name, *value);
3600        }
3601        Response::try_from(builder.body(body).expect("response")).expect("convertible")
3602    }
3603
3604    /// Collects whatever the consumer was handed, keyed by member name.
3605    fn collect<'c>(
3606        strings: &'c mut Vec<(String, String)>,
3607        integers: &'c mut Vec<(String, i32)>,
3608    ) -> impl FnMut(&Schema<'_>, &mut dyn ShapeDeserializer) -> Result<(), SerdeError> + 'c {
3609        move |member, deser| {
3610            let name = member.member_name().unwrap_or("?").to_string();
3611            match member.shape_type() {
3612                ShapeType::Integer => integers.push((name, deser.read_integer(member)?)),
3613                _ => strings.push((name, deser.read_string(member)?)),
3614            }
3615            Ok(())
3616        }
3617    }
3618
3619    #[test]
3620    fn deserialize_response_reads_modeled_headers_and_status() {
3621        let response = response_with(
3622            201,
3623            &[("x-name", "widget")],
3624            aws_smithy_types::body::SdkBody::from("{}"),
3625        );
3626        let mut strings = Vec::new();
3627        let mut integers = Vec::new();
3628        make_protocol()
3629            .deserialize_response(&response, &RESP_BOUND, &ConfigBag::base())
3630            .unwrap()
3631            .read_struct(&RESP_BOUND, &mut collect(&mut strings, &mut integers))
3632            .unwrap();
3633        assert_eq!(strings, vec![("name".to_string(), "widget".to_string())]);
3634        assert_eq!(integers, vec![("status".to_string(), 201)]);
3635    }
3636
3637    #[test]
3638    fn deserialize_error_response_tolerates_an_empty_body_and_still_reads_bindings() {
3639        // The reason this method is overridden rather than left to the default forwarding: a
3640        // service may send a modeled error with no body at all — an S3 `HEAD` is the standard
3641        // case — and the response must still populate its header and status members.
3642        //
3643        // The schema has a body member on purpose. Without one the body is skipped regardless of
3644        // mode, and the default forwarding would pass this test while being wrong.
3645        let response = response_with(
3646            404,
3647            &[("x-name", "missing")],
3648            aws_smithy_types::body::SdkBody::empty(),
3649        );
3650        let mut strings = Vec::new();
3651        let mut integers = Vec::new();
3652        make_protocol()
3653            .deserialize_error_response(&response, &ConfigBag::base())
3654            .unwrap()
3655            .read_struct(&RESP_MIXED, &mut collect(&mut strings, &mut integers))
3656            .expect("an empty error body must not be handed to the codec");
3657        assert_eq!(strings, vec![("name".to_string(), "missing".to_string())]);
3658        assert_eq!(integers, vec![("status".to_string(), 404)]);
3659    }
3660
3661    #[test]
3662    fn deserialize_response_keeps_the_codecs_empty_body_strictness_for_a_success() {
3663        // The counterpart to the test above: the empty-body tolerance is specific to errors, not
3664        // a general loosening. The same response through the success path still reaches the codec.
3665        let response = response_with(
3666            200,
3667            &[("x-name", "here")],
3668            aws_smithy_types::body::SdkBody::empty(),
3669        );
3670        let mut strings = Vec::new();
3671        let mut integers = Vec::new();
3672        let err = make_protocol()
3673            .deserialize_response(&response, &RESP_MIXED, &ConfigBag::base())
3674            .unwrap()
3675            .read_struct(&RESP_MIXED, &mut collect(&mut strings, &mut integers))
3676            .expect_err("a successful response still hands an empty body to the codec");
3677        assert!(
3678            format!("{err}").contains("expected a body"),
3679            "unexpected: {err}"
3680        );
3681    }
3682
3683    #[test]
3684    fn deserialize_response_reads_headers_without_consuming_a_streaming_payload() {
3685        // A streaming response leaves the body unbuffered. The payload member is owned by the
3686        // generated streaming path, so it must be left alone while its siblings are still read —
3687        // this is the case event-stream and streaming-blob outputs miss today.
3688        let response = response_with(
3689            200,
3690            &[("x-name", "stream")],
3691            aws_smithy_types::body::SdkBody::taken(),
3692        );
3693        assert!(
3694            response.body().bytes().is_none(),
3695            "precondition: the body is not buffered"
3696        );
3697        let mut strings = Vec::new();
3698        let mut integers = Vec::new();
3699        make_protocol()
3700            .deserialize_response(&response, &RESP_STREAMING_OUT, &ConfigBag::base())
3701            .unwrap()
3702            .read_struct(
3703                &RESP_STREAMING_OUT,
3704                &mut collect(&mut strings, &mut integers),
3705            )
3706            .unwrap();
3707        assert_eq!(
3708            strings,
3709            vec![("name".to_string(), "stream".to_string())],
3710            "the header must be read and the streaming payload left untouched"
3711        );
3712    }
3713
3714    #[test]
3715    fn deserialize_response_still_hands_a_body_only_output_to_the_codec() {
3716        // The common REST case. `STRUCT_WITH_MEMBER` has one unbound member, so it takes the
3717        // body-only fast path and the returned deserializer is the codec's own.
3718        let response = response_with(
3719            200,
3720            &[],
3721            aws_smithy_types::body::SdkBody::from(r#"{"name":"Bob"}"#),
3722        );
3723        let base_cfg = ConfigBag::base();
3724        let mut deser = make_protocol()
3725            .deserialize_response(&response, &STRUCT_WITH_MEMBER, &base_cfg)
3726            .unwrap();
3727        assert_eq!(deser.read_string(&STRING).unwrap(), r#"{"name":"Bob"}"#);
3728    }
3729}