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}