Skip to main content

aws_smithy_schema/schema/
protocol.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Client protocol traits for protocol-agnostic request serialization and response deserialization.
7//!
8//! [`ClientProtocolInner`] is the trait implementors write. It carries associated
9//! `Request` / `Response` types and allows transport-agnostic protocols (the SEP calls
10//! this out as a requirement).
11//!
12//! [`ClientProtocol`] is the object-safe view that callers use through `dyn`. It's
13//! parameterized over concrete request/response types (defaulted to HTTP) so
14//! [`SharedClientProtocol`] can be stored in a [`ConfigBag`] and swapped at runtime.
15//!
16//! A blanket impl (`impl<P: ClientProtocolInner> ClientProtocol<P::Request, P::Response> for P`)
17//! means implementors only write `ClientProtocolInner`; the object-safe view comes for
18//! free. This mirrors the [`Codec`](crate::codec::Codec) / [`DynCodec`](crate::codec::DynCodec)
19//! pair in the codec module — the same "static-dispatch inner trait + object-safe sibling"
20//! pattern.
21//!
22//! # Implementing a custom protocol
23//!
24//! Third parties can create custom protocols and use them with any client without
25//! modifying a code generator.
26//!
27//! ```ignore
28//! use aws_smithy_schema::protocol::{apply_http_endpoint, ClientProtocolInner};
29//! use aws_smithy_schema::{Schema, ShapeId};
30//! use aws_smithy_schema::serde::SerializableStruct;
31//!
32//! #[derive(Debug)]
33//! struct MyProtocol {
34//!     codec: MyJsonCodec,
35//! }
36//!
37//! impl ClientProtocolInner for MyProtocol {
38//!     type Request = aws_smithy_runtime_api::http::Request;
39//!     type Response = aws_smithy_runtime_api::http::Response;
40//!
41//!     fn protocol_id(&self) -> &ShapeId<'static> { &MY_PROTOCOL_ID }
42//!
43//!     fn serialize_request(
44//!         &self,
45//!         input: &dyn SerializableStruct,
46//!         input_schema: &Schema<'_>,
47//!         endpoint: &str,
48//!         cfg: &ConfigBag,
49//!     ) -> Result<Self::Request, SerdeError> {
50//!         todo!()
51//!     }
52//!
53//!     fn deserialize_response<'a>(
54//!         &self,
55//!         response: &'a Self::Response,
56//!         output_schema: &Schema<'_>,
57//!         cfg: &'a ConfigBag,
58//!     ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
59//!         todo!()
60//!     }
61//!
62//!     fn update_endpoint(
63//!         &self,
64//!         request: &mut Self::Request,
65//!         endpoint: &aws_smithy_types::endpoint::Endpoint,
66//!         cfg: &ConfigBag,
67//!     ) -> Result<(), SerdeError> {
68//!         apply_http_endpoint(request, endpoint, cfg)
69//!     }
70//! }
71//! ```
72
73use crate::serde::{SerdeError, SerializableStruct, ShapeDeserializer};
74use crate::{Schema, ShapeId};
75use aws_smithy_types::config_bag::ConfigBag;
76use aws_smithy_types::endpoint::Endpoint;
77use aws_smithy_types::error::metadata::{Builder as ErrorMetadataBuilder, ErrorMetadata};
78
79/// Statically-dispatched client protocol trait — the one implementors write.
80///
81/// `Request` and `Response` are associated types so a protocol can target any transport
82/// (HTTP, MQTT, Unix-socket, in-memory, …). For the common HTTP case, set both to
83/// `aws_smithy_runtime_api::http::Request` / `Response`.
84///
85/// Callers who need to store a protocol behind `dyn` (e.g., in a [`ConfigBag`] for
86/// runtime swapping) should use the object-safe [`ClientProtocol`] trait instead.
87/// Every `ClientProtocolInner` is automatically a
88/// `ClientProtocol<Self::Request, Self::Response>` via a blanket impl, so implementors
89/// never write `ClientProtocol` manually.
90///
91/// See [`apply_http_endpoint`] for the canonical HTTP implementation of
92/// `update_endpoint`.
93///
94/// # Lifecycle
95///
96/// Instances are immutable and thread-safe. They are typically created once and
97/// shared across all requests for a client.
98pub trait ClientProtocolInner: Send + Sync + std::fmt::Debug {
99    /// The protocol's request message type (e.g., `http::Request`).
100    type Request;
101
102    /// The protocol's response message type (e.g., `http::Response`).
103    type Response;
104
105    /// Returns the Smithy shape ID of this protocol.
106    fn protocol_id(&self) -> &ShapeId<'static>;
107
108    /// Serializes an operation input into a request message.
109    ///
110    /// # The protocol owns its own wire format
111    ///
112    /// Anything this protocol puts on the wire that *the protocol alone determines* must be
113    /// resolved here, not emitted by codegen. A client generated for one protocol can be pointed at
114    /// another with `Config::builder().protocol(..)`, so anything codegen bakes in based on the
115    /// generated protocol is wrong after a swap: left behind when this protocol is swapped out, and
116    /// missing when it is swapped in. Concretely, an implementor owns:
117    ///
118    /// - **Framing headers.** rpcv2Cbor sets `smithy-protocol` and `accept`; awsJson sets
119    ///   `X-Amz-Target`. Codegen's schema path deliberately emits none of these.
120    /// - **The request path**, per the note on `endpoint` below.
121    /// - **Model facts the protocol needs but a caller cannot know**, read from `cfg`:
122    ///   [`ServiceShapeName`], [`ServiceVersion`], [`ServiceXmlNamespace`]. Generated clients store
123    ///   these regardless of which protocol they were generated for, precisely so a swapped-in
124    ///   protocol can find them. Keep an explicit builder as an override for what the model cannot
125    ///   express -- a target prefix that is not the service shape name, for instance.
126    ///
127    /// Headers determined by the *service* rather than the protocol -- `x-amzn-query-mode`, from
128    /// `@awsQueryCompatible` -- are outside this rule and stay in codegen, because their value does
129    /// not change when the protocol does.
130    ///
131    /// # `endpoint` is advisory
132    ///
133    /// `endpoint` is a request **path** (or `""`), never a host; scheme and authority are merged
134    /// later by [`apply_http_endpoint`]. It is whatever codegen computed *for the protocol the
135    /// client was generated for*, so a protocol that alone determines its route must ignore it:
136    ///
137    /// - **Fixed route** -- awsJson and awsQuery are specified to `POST /`, so they pass `/` and
138    ///   ignore the argument entirely. Forwarding it would let an rpcv2Cbor-generated client POST
139    ///   to `/service/{service}/operation/{operation}`.
140    /// - **Route derived from model facts** -- rpcv2Cbor computes
141    ///   `/service/{service}/operation/{operation}` from `cfg`, falling back to `endpoint` only
142    ///   when those facts are absent.
143    /// - **Route from `@http` bindings** -- REST protocols expand the operation's `@http` template
144    ///   from the schema, which is authoritative; `endpoint` is ignored. Generated REST clients
145    ///   pass `""`, so this costs them nothing, and it stops an RPC route from being prefixed onto
146    ///   the template after a swap. `endpoint` acts as the template only for a schema that carries
147    ///   no `@http` trait at all.
148    ///
149    /// The shared [`HttpRpcProtocol`](crate::schema::http_protocol::HttpRpcProtocol) helper
150    /// deliberately does *not* hard-code `/`; only a concrete protocol knows whether its route is
151    /// constant.
152    ///
153    /// [`ServiceShapeName`]: crate::protocol::ServiceShapeName
154    /// [`ServiceVersion`]: crate::protocol::ServiceVersion
155    /// [`ServiceXmlNamespace`]: crate::protocol::ServiceXmlNamespace
156    fn serialize_request(
157        &self,
158        input: &dyn SerializableStruct,
159        input_schema: &Schema<'_>,
160        endpoint: &str,
161        cfg: &ConfigBag,
162    ) -> Result<Self::Request, SerdeError>;
163
164    /// Deserializes a response message, returning a boxed [`ShapeDeserializer`] that supplies
165    /// every member of `output_schema` this protocol carries in the response.
166    ///
167    /// The protocol, not the caller, decides where each member comes from. A protocol that uses
168    /// HTTP bindings reads `@httpHeader`, `@httpPrefixHeaders`, `@httpResponseCode` and
169    /// `@httpPayload` members from the HTTP message and the rest from the body (see
170    /// [`http_output_deserializer`](crate::http_protocol::http_output_deserializer)); a body-only
171    /// protocol ignores those traits and reads every member from the body. Generated code hands
172    /// the result to the output builder's member consumer and does no transport parsing itself.
173    ///
174    /// A `@streaming` payload member is never supplied: the caller owns the live body.
175    ///
176    /// `cfg` shares the returned deserializer's lifetime so a protocol can defer reading
177    /// response-scoped settings until they are needed; the HTTP-binding composite reads
178    /// `NonUtf8HeaderHandling` only after a header fails to parse. An implementation that does
179    /// not keep `cfg` may declare it as a plain `&ConfigBag`.
180    fn deserialize_response<'a>(
181        &self,
182        response: &'a Self::Response,
183        output_schema: &Schema<'_>,
184        cfg: &'a ConfigBag,
185    ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError>;
186
187    /// Extracts canonical error metadata (code, message, request id) from a
188    /// response's wire envelope.
189    ///
190    /// Returns a [`Builder`](ErrorMetadataBuilder) so callers can attach
191    /// per-request fields (e.g., `x-amzn-RequestId` from an HTTP header) before
192    /// finalizing.
193    ///
194    /// Concrete protocols override this to extract their envelope-specific
195    /// fields:
196    /// - awsJson1.0 / awsJson1.1: `__type` from the body, `X-Amzn-Errortype`
197    ///   header fallback.
198    /// - restJson1: same as awsJson.
199    /// - restXml (wrapped): `<ErrorResponse><Error><Code>` etc.
200    /// - restXml (`@restXml(noErrorWrapping: true)`): `<Error><Code>` etc.
201    /// - awsQuery / ec2Query: `<ErrorResponse><Error><Code>` etc.
202    /// - rpcv2Cbor: `__type` from the CBOR map.
203    ///
204    /// The default implementation returns an empty
205    /// [`Builder`](ErrorMetadataBuilder) — sufficient for protocols that
206    /// haven't migrated to schema-driven error dispatch yet, but
207    /// callers will see `Option::None` for `code()` / `message()` and treat
208    /// the response as an unhandled error.
209    fn parse_error_metadata(
210        &self,
211        response: &Self::Response,
212        cfg: &ConfigBag,
213    ) -> Result<ErrorMetadataBuilder, SerdeError> {
214        let _ = (response, cfg);
215        Ok(ErrorMetadata::builder())
216    }
217
218    /// Returns a [`ShapeDeserializer`] positioned at the body of an error
219    /// response — *inside* the protocol's error envelope, where applicable.
220    ///
221    /// Generated error dispatch code calls this to obtain a deserializer for the modeled error's
222    /// member consumer, regardless of which protocol is active at runtime. As with
223    /// [`deserialize_response`](Self::deserialize_response), the protocol decides which members
224    /// come from the transport and which from the body.
225    ///
226    /// For **body-only** protocols (awsJson1.0/1.1, awsQuery, ec2Query, rpcv2Cbor) the default
227    /// implementation suffices: the body root *is* the error body, so it forwards to
228    /// [`deserialize_response`](Self::deserialize_response) against
229    /// [`prelude::DOCUMENT`](crate::prelude::DOCUMENT), and HTTP response bindings are correctly
230    /// ignored.
231    ///
232    /// Envelope-bearing protocols (restXml wrapped / unwrapped, awsQuery, ec2Query) MUST
233    /// override to strip the outer `<ErrorResponse>` / `<Error>` wrapper before returning the
234    /// deserializer.
235    ///
236    /// # Protocols that use HTTP bindings must override this
237    ///
238    /// A protocol whose errors carry `@httpHeader`, `@httpPrefixHeaders` or
239    /// `@httpResponseCode` members MUST override this and build its deserializer with
240    /// [`http_error_deserializer`](crate::http_protocol::http_error_deserializer), even when its
241    /// body needs no envelope positioning. The default's two properties are both accidents
242    /// rather than contracts: it evaluates the *success* body-only fast path, and it does so
243    /// against a schema that is not the error's. Neither is safe to depend on, because the
244    /// concrete error schema is unknown until the generated error variant calls `read_struct`.
245    /// Every built-in REST protocol therefore overrides this, and
246    /// `the_error_path_never_takes_the_body_only_fast_path` pins the consequence.
247    fn deserialize_error_response<'a>(
248        &self,
249        response: &'a Self::Response,
250        cfg: &'a ConfigBag,
251    ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
252        self.deserialize_response(response, &crate::prelude::DOCUMENT, cfg)
253    }
254
255    /// Updates a previously serialized request with a resolved endpoint.
256    ///
257    /// Required by SEP requirement 7. The orchestrator calls this after endpoint
258    /// resolution, which happens *after* `serialize_request`.
259    ///
260    /// HTTP protocols should implement this as:
261    /// ```ignore
262    /// apply_http_endpoint(request, endpoint, cfg)
263    /// ```
264    /// (See [`apply_http_endpoint`].) Non-HTTP protocols implement the transport's
265    /// equivalent.
266    fn update_endpoint(
267        &self,
268        request: &mut Self::Request,
269        endpoint: &Endpoint,
270        cfg: &ConfigBag,
271    ) -> Result<(), SerdeError>;
272
273    /// Returns the codec used for payload (de)serialization, if any.
274    ///
275    /// See [`DynCodec`](crate::codec::DynCodec) for why the codec is exposed
276    /// through the object-safe sibling.
277    fn payload_codec(&self) -> Option<&dyn crate::codec::DynCodec> {
278        None
279    }
280
281    /// The media type used to label a **structured event-stream payload**, if this
282    /// protocol supports event streams — for example `application/cbor` or
283    /// `application/json`.
284    ///
285    /// Every event-stream frame carries a `:content-type` header describing its
286    /// payload. The payload itself is encoded by whichever protocol is selected at
287    /// runtime, so this label has to come from that same protocol; a label baked in
288    /// when the client was generated contradicts the bytes as soon as a different
289    /// protocol is selected, and a peer that honours the header then decodes with
290    /// the wrong codec.
291    ///
292    /// # Why this is on the protocol and not on [`Codec`](crate::codec::Codec)
293    ///
294    /// The value is protocol-determined, not format-determined, and one codec
295    /// serves several protocols that disagree about it. A single `JsonCodec` backs
296    /// restJson1, awsJson1_0 and awsJson1_1, whose request content types are
297    /// `application/json`, `application/x-amz-json-1.0` and
298    /// `application/x-amz-json-1.1` respectively. A codec-level accessor could
299    /// therefore only ever return one of those, and would describe event payloads
300    /// correctly only because the three JSON protocols happen to agree *there*.
301    ///
302    /// # What this is not
303    ///
304    /// This is **not** the request `Content-Type`. For awsJson the two differ, per
305    /// the above. It is also not the content type of a payload whose type is fixed
306    /// by the *shape* rather than the format: an `@eventPayload` blob is
307    /// `application/octet-stream` and a string is `text/plain` under every
308    /// protocol, so those stay with the code generator and must not be sourced
309    /// from here.
310    ///
311    /// Returns `None` by default, meaning the protocol declares no such media type
312    /// — callers must keep a fallback. The default keeps this addition
313    /// non-breaking for third-party protocols, which the SEP requires be able to
314    /// exist without modifying a code generator.
315    fn event_stream_media_type(&self) -> Option<&str> {
316        None
317    }
318
319    /// Extracts canonical error metadata from the payload of an event-stream
320    /// `exception` frame.
321    ///
322    /// This is the event-stream counterpart of
323    /// [`parse_error_metadata`](Self::parse_error_metadata), and it exists
324    /// separately for a typing reason rather than a behavioral one: that method
325    /// takes `&Self::Response`, an HTTP response, and an event-stream frame is not
326    /// one. The parsing itself is identical — both read a protocol-specific error
327    /// envelope out of a byte payload — so implementors should delegate to the same
328    /// helper they use there.
329    ///
330    /// # Why this must be resolved by the protocol
331    ///
332    /// The frame's payload is encoded by whichever protocol is selected at runtime
333    /// (via [`payload_codec`](Self::payload_codec)), so its error envelope must be
334    /// parsed by that same protocol. A code generator cannot decide this: it knows
335    /// only the protocol the client was generated for, and after a runtime protocol
336    /// swap that is the wrong one. Getting it wrong is not silent — the parse fails
337    /// and the caller reports an unhandled error — but it costs the error code, and
338    /// with it the modeled error variant and any retry classification keyed on that
339    /// code.
340    ///
341    /// # Scope
342    ///
343    /// Takes the payload only. An event-stream frame carries no HTTP headers, so
344    /// there is nothing to pass for the header-borne discriminators some protocols
345    /// also accept (restJson1's `x-amzn-errortype`, awsQuery-compatible's
346    /// `x-amzn-query-error`); implementors should parse as though the header map
347    /// were empty. The frame's own `:exception-type` header is a separate mechanism
348    /// and is handled by generated dispatch code before this is called.
349    ///
350    /// The default returns an empty
351    /// [`Builder`](ErrorMetadataBuilder), matching
352    /// [`parse_error_metadata`](Self::parse_error_metadata): callers see
353    /// `Option::None` for `code()` / `message()` and treat the frame as an
354    /// unhandled error. The default keeps this addition non-breaking for
355    /// third-party protocols, which the SEP requires be able to exist without
356    /// modifying a code generator.
357    ///
358    /// Implementors of a protocol without event streams should leave this alone.
359    fn parse_event_stream_error_metadata(
360        &self,
361        payload: &[u8],
362    ) -> Result<ErrorMetadataBuilder, SerdeError> {
363        let _ = payload;
364        Ok(ErrorMetadata::builder())
365    }
366}
367
368/// Object-safe view of [`ClientProtocolInner`] parameterized over concrete
369/// request / response types.
370///
371/// This is what callers hold behind `dyn`, for example,
372/// [`SharedClientProtocol`] stores `Arc<dyn ClientProtocol<Req, Res>>` so the
373/// protocol can be swapped at runtime. The generic `Req` / `Res` parameters
374/// default to HTTP so existing call sites remain source-compatible.
375///
376/// Every `ClientProtocolInner` gets `ClientProtocol` for free via a blanket
377/// impl; implementors should write `ClientProtocolInner` only.
378pub trait ClientProtocol<
379    Req = aws_smithy_runtime_api::http::Request,
380    Res = aws_smithy_runtime_api::http::Response,
381>: Send + Sync + std::fmt::Debug
382{
383    /// Returns the Smithy shape ID of this protocol.
384    fn protocol_id(&self) -> &ShapeId<'static>;
385
386    /// Serializes an operation input into a request message.
387    ///
388    /// See [`ClientProtocolInner::serialize_request`] for the invariant an implementor must uphold:
389    /// the protocol owns its own framing headers and request path, and `endpoint` is advisory.
390    fn serialize_request(
391        &self,
392        input: &dyn SerializableStruct,
393        input_schema: &Schema<'_>,
394        endpoint: &str,
395        cfg: &ConfigBag,
396    ) -> Result<Req, SerdeError>;
397
398    /// Deserializes a response message, returning a boxed [`ShapeDeserializer`].
399    fn deserialize_response<'a>(
400        &self,
401        response: &'a Res,
402        output_schema: &Schema<'_>,
403        cfg: &'a ConfigBag,
404    ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError>;
405
406    /// Extracts canonical error metadata from a response.
407    ///
408    /// See [`ClientProtocolInner::parse_error_metadata`] for the contract.
409    fn parse_error_metadata(
410        &self,
411        response: &Res,
412        cfg: &ConfigBag,
413    ) -> Result<ErrorMetadataBuilder, SerdeError>;
414
415    /// Returns a [`ShapeDeserializer`] positioned at the body of an error
416    /// response — inside the protocol's error envelope, where applicable.
417    ///
418    /// See [`ClientProtocolInner::deserialize_error_response`] for the contract.
419    fn deserialize_error_response<'a>(
420        &self,
421        response: &'a Res,
422        cfg: &'a ConfigBag,
423    ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError>;
424
425    /// Updates a previously serialized request with a resolved endpoint.
426    fn update_endpoint(
427        &self,
428        request: &mut Req,
429        endpoint: &Endpoint,
430        cfg: &ConfigBag,
431    ) -> Result<(), SerdeError>;
432
433    /// Returns the codec used for payload (de)serialization, if any.
434    fn payload_codec(&self) -> Option<&dyn crate::codec::DynCodec>;
435
436    /// The media type used to label a structured event-stream payload, if any.
437    ///
438    /// See [`ClientProtocolInner::event_stream_media_type`] for why this is a
439    /// protocol-level fact rather than a codec-level one.
440    fn event_stream_media_type(&self) -> Option<&str>;
441
442    /// Extracts canonical error metadata from an event-stream `exception` frame's
443    /// payload.
444    ///
445    /// See [`ClientProtocolInner::parse_event_stream_error_metadata`] for the
446    /// contract and for why the payload's envelope must be parsed by the protocol
447    /// selected at runtime.
448    fn parse_event_stream_error_metadata(
449        &self,
450        payload: &[u8],
451    ) -> Result<ErrorMetadataBuilder, SerdeError>;
452}
453
454// Blanket impl: any `ClientProtocolInner` is automatically a `ClientProtocol`
455// parameterized over its associated `Request` / `Response` types.
456impl<P> ClientProtocol<P::Request, P::Response> for P
457where
458    P: ClientProtocolInner,
459{
460    fn protocol_id(&self) -> &ShapeId<'static> {
461        <Self as ClientProtocolInner>::protocol_id(self)
462    }
463
464    fn serialize_request(
465        &self,
466        input: &dyn SerializableStruct,
467        input_schema: &Schema<'_>,
468        endpoint: &str,
469        cfg: &ConfigBag,
470    ) -> Result<P::Request, SerdeError> {
471        <Self as ClientProtocolInner>::serialize_request(self, input, input_schema, endpoint, cfg)
472    }
473
474    fn deserialize_response<'a>(
475        &self,
476        response: &'a P::Response,
477        output_schema: &Schema<'_>,
478        cfg: &'a ConfigBag,
479    ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
480        <Self as ClientProtocolInner>::deserialize_response(self, response, output_schema, cfg)
481    }
482
483    fn parse_error_metadata(
484        &self,
485        response: &P::Response,
486        cfg: &ConfigBag,
487    ) -> Result<ErrorMetadataBuilder, SerdeError> {
488        <Self as ClientProtocolInner>::parse_error_metadata(self, response, cfg)
489    }
490
491    fn deserialize_error_response<'a>(
492        &self,
493        response: &'a P::Response,
494        cfg: &'a ConfigBag,
495    ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
496        <Self as ClientProtocolInner>::deserialize_error_response(self, response, cfg)
497    }
498
499    fn update_endpoint(
500        &self,
501        request: &mut P::Request,
502        endpoint: &Endpoint,
503        cfg: &ConfigBag,
504    ) -> Result<(), SerdeError> {
505        <Self as ClientProtocolInner>::update_endpoint(self, request, endpoint, cfg)
506    }
507
508    fn payload_codec(&self) -> Option<&dyn crate::codec::DynCodec> {
509        <Self as ClientProtocolInner>::payload_codec(self)
510    }
511
512    fn event_stream_media_type(&self) -> Option<&str> {
513        <Self as ClientProtocolInner>::event_stream_media_type(self)
514    }
515
516    fn parse_event_stream_error_metadata(
517        &self,
518        payload: &[u8],
519    ) -> Result<ErrorMetadataBuilder, SerdeError> {
520        <Self as ClientProtocolInner>::parse_event_stream_error_metadata(self, payload)
521    }
522}
523
524/// Applies a resolved endpoint to an HTTP request.
525///
526/// This is the canonical HTTP implementation of
527/// [`ClientProtocolInner::update_endpoint`]. HTTP protocols should delegate to it.
528///
529/// Handles endpoint prefixes (for `EndpointPrefix`-enabled operations) and
530/// endpoint-supplied headers.
531pub fn apply_http_endpoint(
532    request: &mut aws_smithy_runtime_api::http::Request,
533    endpoint: &Endpoint,
534    cfg: &ConfigBag,
535) -> Result<(), SerdeError> {
536    use std::borrow::Cow;
537
538    let endpoint_prefix = cfg.load::<aws_smithy_runtime_api::client::endpoint::EndpointPrefix>();
539    let endpoint_url = match endpoint_prefix {
540        None => Cow::Borrowed(endpoint.url()),
541        Some(prefix) => {
542            let parsed: http::Uri = endpoint
543                .url()
544                .parse()
545                .map_err(|e| SerdeError::custom(format!("invalid endpoint URI: {e}")))?;
546            let scheme = parsed.scheme_str().unwrap_or_default();
547            let prefix = prefix.as_str();
548            let authority = parsed.authority().map(|a| a.as_str()).unwrap_or_default();
549            let path_and_query = parsed
550                .path_and_query()
551                .map(|pq| pq.as_str())
552                .unwrap_or_default();
553            Cow::Owned(format!("{scheme}://{prefix}{authority}{path_and_query}"))
554        }
555    };
556
557    request.uri_mut().set_endpoint(&endpoint_url).map_err(|e| {
558        SerdeError::custom(format!("failed to apply endpoint `{endpoint_url}`: {e}"))
559    })?;
560
561    for (header_name, header_values) in endpoint.headers() {
562        request.headers_mut().remove(header_name);
563        for value in header_values {
564            request
565                .headers_mut()
566                .append(header_name.to_owned(), value.to_owned());
567        }
568    }
569
570    Ok(())
571}
572
573/// The name of the Smithy `service` shape a client was generated for.
574///
575/// Some protocols derive parts of the wire format from model names rather than
576/// from HTTP binding traits. RPC v2 CBOR is the canonical example: every request
577/// is routed to `/service/{serviceName}/operation/{operationName}`, where
578/// `serviceName` is the *service shape name* — not the `@aws.api#service`
579/// `sdkId`, and not the shape's namespace.
580///
581/// Because [`SharedClientProtocol`] can be swapped at runtime, a protocol cannot
582/// rely on codegen having baked its route into the generated request path: a
583/// client generated for `awsJson1_0` may have `RpcV2CborProtocol` plugged in via
584/// `Config::builder().protocol(..)`. Generated clients therefore store this entry
585/// in the config bag regardless of which protocol they were generated for, so
586/// whichever protocol ends up being used can resolve the names it needs. The
587/// companion operation name comes from
588/// [`Metadata::name`](aws_smithy_runtime_api::client::orchestrator::Metadata::name).
589///
590/// See <https://github.com/smithy-lang/smithy-rs/issues/4801>.
591#[derive(Debug, Clone, PartialEq, Eq)]
592#[non_exhaustive]
593pub struct ServiceShapeName(std::borrow::Cow<'static, str>);
594
595impl ServiceShapeName {
596    /// Creates a new [`ServiceShapeName`] from the Smithy service shape name.
597    ///
598    /// Accepts a codegen-emitted `&'static str` as well as a `String`
599    /// materialized at runtime from a parsed model.
600    pub fn new(name: impl Into<std::borrow::Cow<'static, str>>) -> Self {
601        Self(name.into())
602    }
603
604    /// Returns the service shape name.
605    pub fn as_str(&self) -> &str {
606        &self.0
607    }
608}
609
610impl aws_smithy_types::config_bag::Storable for ServiceShapeName {
611    type Storer = aws_smithy_types::config_bag::StoreReplace<Self>;
612}
613
614/// The namespace of the Smithy `service` shape a client was generated for.
615///
616/// This is the `com.amazonaws.dynamodb` in `com.amazonaws.dynamodb#DynamoDB_20120810`.
617/// Together with [`ServiceShapeName`] it forms the service's full shape ID; the two are
618/// separate entries rather than one because [`ConfigBag`] is keyed by type, so each protocol
619/// loads exactly the facts it needs and new facts stay additive.
620///
621/// **Not to be confused with [`ServiceXmlNamespace`]**, despite the shared word. That one is
622/// the `@xmlNamespace` *trait* — a URI restXml applies as the default `xmlns` on root
623/// elements — and neither value is derivable from the other. CloudWatch Logs is the clearest
624/// illustration: its shape-ID namespace is `com.amazonaws.cloudwatchlogs` while its
625/// `@xmlNamespace` URI is `http://monitoring.amazonaws.com/doc/2014-03-28/`. The trait is
626/// also optional, carried by roughly half of AWS service shapes, whereas every shape ID has
627/// a namespace by construction — which is why this entry is stored unconditionally and
628/// `ServiceXmlNamespace` is not.
629///
630/// Protocols use this as the *default namespace* when resolving a document type's shape
631/// discriminator. Some services serialize a discriminator as a bare shape name rather than an
632/// absolute shape ID — a `__type` of `Widget` instead of `com.example#Widget` — and the
633/// receiving client is expected to qualify it with the service's namespace. Without it a
634/// relative discriminator cannot be resolved to a registered type at all.
635///
636/// Stored for the same reason as [`ServiceShapeName`]: it is knowable only from the model, and
637/// a customer may select a different protocol at runtime via `Config::builder().protocol(..)`
638/// on a client generated for some other protocol. Baking it into a constructor call at codegen
639/// time means a swapped-in protocol either gets no value or gets one the caller had to know to
640/// supply. Generated clients therefore store it regardless of which protocol they were
641/// generated for.
642#[derive(Debug, Clone, PartialEq, Eq)]
643#[non_exhaustive]
644pub struct ServiceShapeNamespace(std::borrow::Cow<'static, str>);
645
646impl ServiceShapeNamespace {
647    /// Creates a new [`ServiceShapeNamespace`] from the Smithy service shape's namespace.
648    ///
649    /// Accepts a codegen-emitted `&'static str` as well as a `String` materialized at runtime
650    /// from a parsed model.
651    pub fn new(namespace: impl Into<std::borrow::Cow<'static, str>>) -> Self {
652        Self(namespace.into())
653    }
654
655    /// Returns the service shape's namespace.
656    pub fn as_str(&self) -> &str {
657        &self.0
658    }
659}
660
661impl aws_smithy_types::config_bag::Storable for ServiceShapeNamespace {
662    type Storer = aws_smithy_types::config_bag::StoreReplace<Self>;
663}
664
665/// The Smithy service shape's `version`, stored in a [`ConfigBag`] by generated clients.
666///
667/// awsQuery puts this on the wire as the `Version=` form parameter, so it is a request-shaping
668/// fact that only the model knows. Stored for the same reason as [`ServiceShapeName`]: a customer
669/// can select awsQuery via `Config::builder().protocol(..)` on a client generated for some other
670/// protocol, and could not otherwise supply the right value.
671#[derive(Debug, Clone, PartialEq, Eq)]
672#[non_exhaustive]
673pub struct ServiceVersion(std::borrow::Cow<'static, str>);
674
675impl ServiceVersion {
676    /// Creates a new [`ServiceVersion`].
677    ///
678    /// Accepts a codegen-emitted `&'static str` as well as a `String` materialized at runtime
679    /// from a parsed model.
680    pub fn new(version: impl Into<std::borrow::Cow<'static, str>>) -> Self {
681        Self(version.into())
682    }
683
684    /// Returns the service version.
685    pub fn as_str(&self) -> &str {
686        &self.0
687    }
688}
689
690impl aws_smithy_types::config_bag::Storable for ServiceVersion {
691    type Storer = aws_smithy_types::config_bag::StoreReplace<Self>;
692}
693
694/// The service-level `@xmlNamespace` trait, stored in a [`ConfigBag`] by generated clients.
695///
696/// restXml applies this as the default `xmlns` on request and response root elements, so it is a
697/// request-shaping fact that only the model knows. Stored for the same reason as
698/// [`ServiceShapeName`].
699///
700/// `@xmlNamespace` is a prelude trait rather than a restXml-specific one, so it is resolvable from
701/// any model — unlike `@restXml(noErrorWrapping)`, which a non-restXml model simply does not carry
702/// and which therefore stays caller-supplied.
703#[derive(Debug, Clone, PartialEq, Eq)]
704#[non_exhaustive]
705pub struct ServiceXmlNamespace {
706    uri: std::borrow::Cow<'static, str>,
707    prefix: Option<std::borrow::Cow<'static, str>>,
708}
709
710impl ServiceXmlNamespace {
711    /// Creates a new [`ServiceXmlNamespace`] from the trait's URI and optional prefix.
712    pub fn new(
713        uri: impl Into<std::borrow::Cow<'static, str>>,
714        prefix: Option<std::borrow::Cow<'static, str>>,
715    ) -> Self {
716        Self {
717            uri: uri.into(),
718            prefix,
719        }
720    }
721
722    /// Returns the namespace URI.
723    pub fn uri(&self) -> &str {
724        &self.uri
725    }
726
727    /// Returns the namespace prefix, if the trait declared one.
728    pub fn prefix(&self) -> Option<&str> {
729        self.prefix.as_deref()
730    }
731}
732
733impl aws_smithy_types::config_bag::Storable for ServiceXmlNamespace {
734    type Storer = aws_smithy_types::config_bag::StoreReplace<Self>;
735}
736
737/// A shared, type-erased client protocol stored in a [`ConfigBag`].
738///
739/// Wraps `Arc<dyn ClientProtocol<Req, Res>>` so a protocol can be stored and
740/// retrieved from the config bag for runtime protocol selection.
741///
742/// Defaults to HTTP transport types. Custom transports would use
743/// `SharedClientProtocol<MyReq, MyRes>` and would need their own `Storable`
744/// adaptation (not provided here — today only HTTP has a `Storable` impl,
745/// reflecting the fact that the orchestrator is HTTP-concrete).
746#[derive(Debug)]
747pub struct SharedClientProtocol<
748    Req = aws_smithy_runtime_api::http::Request,
749    Res = aws_smithy_runtime_api::http::Response,
750> {
751    inner: std::sync::Arc<dyn ClientProtocol<Req, Res>>,
752}
753
754// Manual `Clone` — `Arc` is cheaply cloneable regardless of whether the inner
755// `Req` / `Res` types are themselves `Clone`, so this impl avoids a spurious
756// `Req: Clone, Res: Clone` bound that `#[derive(Clone)]` would introduce.
757impl<Req, Res> Clone for SharedClientProtocol<Req, Res> {
758    fn clone(&self) -> Self {
759        Self {
760            inner: self.inner.clone(),
761        }
762    }
763}
764
765impl<Req, Res> SharedClientProtocol<Req, Res>
766where
767    Req: 'static,
768    Res: 'static,
769{
770    /// Creates a new shared protocol from any [`ClientProtocol<Req, Res>`] impl.
771    ///
772    /// In practice callers pass a concrete type that implements
773    /// [`ClientProtocolInner`] — the blanket `impl<P: ClientProtocolInner>
774    /// ClientProtocol<P::Request, P::Response> for P` makes every
775    /// `ClientProtocolInner` automatically usable here.
776    pub fn new<P>(protocol: P) -> Self
777    where
778        P: ClientProtocol<Req, Res> + 'static,
779    {
780        Self {
781            inner: std::sync::Arc::new(protocol),
782        }
783    }
784}
785
786impl<Req, Res> std::ops::Deref for SharedClientProtocol<Req, Res> {
787    type Target = dyn ClientProtocol<Req, Res>;
788
789    fn deref(&self) -> &Self::Target {
790        &*self.inner
791    }
792}
793
794// Only the HTTP specialization is storable in the config bag, matching the
795// orchestrator's HTTP-concrete wiring today. This is paired with the three
796// `protocol(…)` setters — `aws_types::SdkConfig::Builder::protocol`,
797// `aws_config::ConfigLoader::protocol`, and the generated
798// `ConfigBuilder::protocol` — all of which accept `impl ClientProtocol +
799// 'static` (resolving via defaults to the HTTP specialization) and store
800// the resulting `SharedClientProtocol<http::Request, http::Response>` here.
801//
802// Non-HTTP transports would add their own Storable newtype alongside their
803// transport integration (with its own dedicated setter) rather than
804// generalizing this impl — see §10.2 of the implementation overview.
805impl aws_smithy_types::config_bag::Storable
806    for SharedClientProtocol<
807        aws_smithy_runtime_api::http::Request,
808        aws_smithy_runtime_api::http::Response,
809    >
810{
811    type Storer = aws_smithy_types::config_bag::StoreReplace<Self>;
812}
813
814#[cfg(test)]
815mod tests {
816    use super::*;
817    use crate::serde::{SerdeError, SerializableStruct, ShapeDeserializer};
818    use crate::{Schema, ShapeId};
819    use aws_smithy_runtime_api::http::{Request, Response, StatusCode};
820    use aws_smithy_types::body::SdkBody;
821    use aws_smithy_types::config_bag::{ConfigBag, Layer};
822    use aws_smithy_types::endpoint::Endpoint;
823
824    /// Minimal protocol impl that uses the HTTP apply_http_endpoint helper.
825    #[derive(Debug)]
826    struct StubProtocol;
827
828    static STUB_ID: ShapeId<'static> =
829        ShapeId::from_parts("test#StubProtocol", "test", "StubProtocol");
830
831    impl ClientProtocolInner for StubProtocol {
832        type Request = Request;
833        type Response = Response;
834
835        fn protocol_id(&self) -> &ShapeId<'static> {
836            &STUB_ID
837        }
838        fn serialize_request(
839            &self,
840            _input: &dyn SerializableStruct,
841            _input_schema: &Schema<'_>,
842            _endpoint: &str,
843            _cfg: &ConfigBag,
844        ) -> Result<Request, SerdeError> {
845            unimplemented!()
846        }
847        fn deserialize_response<'a>(
848            &self,
849            _response: &'a Response,
850            _output_schema: &Schema<'_>,
851            // Deliberately without `'a`: an implementation that does not keep the bag may
852            // keep the pre-`'a` signature, and this pins that it still compiles.
853            _cfg: &ConfigBag,
854        ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
855            unimplemented!()
856        }
857        fn update_endpoint(
858            &self,
859            request: &mut Request,
860            endpoint: &Endpoint,
861            cfg: &ConfigBag,
862        ) -> Result<(), SerdeError> {
863            apply_http_endpoint(request, endpoint, cfg)
864        }
865    }
866
867    fn request_with_uri(uri: &str) -> Request {
868        let mut req = Request::new(SdkBody::empty());
869        req.set_uri(uri).unwrap();
870        req
871    }
872
873    #[test]
874    fn basic_endpoint() {
875        let proto = StubProtocol;
876        let mut req = request_with_uri("/original/path");
877        let endpoint = Endpoint::builder()
878            .url("https://service.us-east-1.amazonaws.com")
879            .build();
880        let cfg = ConfigBag::base();
881
882        ClientProtocolInner::update_endpoint(&proto, &mut req, &endpoint, &cfg).unwrap();
883        assert_eq!(
884            req.uri(),
885            "https://service.us-east-1.amazonaws.com/original/path"
886        );
887    }
888
889    #[test]
890    fn endpoint_with_prefix() {
891        let proto = StubProtocol;
892        let mut req = request_with_uri("/path");
893        let endpoint = Endpoint::builder()
894            .url("https://service.us-east-1.amazonaws.com")
895            .build();
896        let mut cfg = ConfigBag::base();
897        let mut layer = Layer::new("test");
898        layer.store_put(
899            aws_smithy_runtime_api::client::endpoint::EndpointPrefix::new("myprefix.").unwrap(),
900        );
901        cfg.push_shared_layer(layer.freeze());
902
903        ClientProtocolInner::update_endpoint(&proto, &mut req, &endpoint, &cfg).unwrap();
904        assert_eq!(
905            req.uri(),
906            "https://myprefix.service.us-east-1.amazonaws.com/path"
907        );
908    }
909
910    #[test]
911    fn endpoint_with_headers() {
912        let proto = StubProtocol;
913        let mut req = request_with_uri("/path");
914        let endpoint = Endpoint::builder()
915            .url("https://example.com")
916            .header("x-custom", "value1")
917            .header("x-custom", "value2")
918            .build();
919        let cfg = ConfigBag::base();
920
921        ClientProtocolInner::update_endpoint(&proto, &mut req, &endpoint, &cfg).unwrap();
922        assert_eq!(req.uri(), "https://example.com/path");
923        let values: Vec<&str> = req.headers().get_all("x-custom").collect();
924        assert_eq!(values, vec!["value1", "value2"]);
925    }
926
927    #[test]
928    fn endpoint_with_path() {
929        let proto = StubProtocol;
930        let mut req = request_with_uri("/operation");
931        let endpoint = Endpoint::builder().url("https://example.com/base").build();
932        let cfg = ConfigBag::base();
933
934        ClientProtocolInner::update_endpoint(&proto, &mut req, &endpoint, &cfg).unwrap();
935        assert_eq!(req.uri(), "https://example.com/base/operation");
936    }
937
938    // -- Default impls for parse_error_metadata + deserialize_error_response --
939
940    #[test]
941    fn parse_error_metadata_default_returns_empty_builder() {
942        let proto = StubProtocol;
943        let response = Response::new(StatusCode::try_from(500).unwrap(), SdkBody::empty());
944        let cfg = ConfigBag::base();
945
946        let builder = ClientProtocolInner::parse_error_metadata(&proto, &response, &cfg).unwrap();
947        let meta = builder.build();
948        assert!(meta.code().is_none());
949        assert!(meta.message().is_none());
950    }
951
952    /// Records the [`Schema`] id passed to `deserialize_response` so the
953    /// `deserialize_error_response` default forwarding can be asserted.
954    /// Captures the FQN as a `String` so the fixture isn't tied to the
955    /// schema's data lifetime.
956    #[derive(Debug, Default)]
957    struct RecordingProtocol {
958        last_schema_id: std::sync::Mutex<Option<String>>,
959    }
960
961    static REC_ID: ShapeId<'static> =
962        ShapeId::from_parts("test#RecordingProtocol", "test", "RecordingProtocol");
963
964    impl ClientProtocolInner for RecordingProtocol {
965        type Request = Request;
966        type Response = Response;
967
968        fn protocol_id(&self) -> &ShapeId<'static> {
969            &REC_ID
970        }
971        fn serialize_request(
972            &self,
973            _input: &dyn SerializableStruct,
974            _input_schema: &Schema<'_>,
975            _endpoint: &str,
976            _cfg: &ConfigBag,
977        ) -> Result<Request, SerdeError> {
978            unimplemented!()
979        }
980        fn deserialize_response<'a>(
981            &self,
982            _response: &'a Response,
983            output_schema: &Schema<'_>,
984            _cfg: &'a ConfigBag,
985        ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
986            *self
987                .last_schema_id
988                .lock()
989                .expect("RecordingProtocol mutex poisoned") =
990                Some(output_schema.shape_id().as_str().to_owned());
991            // Return an Err so we don't have to construct a real deserializer;
992            // the test only cares which schema was forwarded.
993            Err(SerdeError::custom("recording stub"))
994        }
995        fn update_endpoint(
996            &self,
997            _request: &mut Request,
998            _endpoint: &Endpoint,
999            _cfg: &ConfigBag,
1000        ) -> Result<(), SerdeError> {
1001            unimplemented!()
1002        }
1003    }
1004
1005    #[test]
1006    fn deserialize_error_response_default_forwards_with_prelude_document_schema() {
1007        let proto = RecordingProtocol::default();
1008        let response = Response::new(StatusCode::try_from(500).unwrap(), SdkBody::empty());
1009        let cfg = ConfigBag::base();
1010
1011        // The default impl forwards to deserialize_response. Our recording
1012        // stub captures the schema and then returns an error — we don't
1013        // care about the result, only the schema observed.
1014        let _ = ClientProtocolInner::deserialize_error_response(&proto, &response, &cfg);
1015
1016        let observed = proto
1017            .last_schema_id
1018            .lock()
1019            .expect("RecordingProtocol mutex poisoned")
1020            .clone()
1021            .expect("schema id was captured");
1022        assert_eq!(observed, crate::prelude::DOCUMENT.shape_id().as_str());
1023    }
1024}