Skip to main content

ClientProtocolInner

Trait ClientProtocolInner 

Source
pub trait ClientProtocolInner:
    Send
    + Sync
    + Debug {
    type Request;
    type Response;

    // Required methods
    fn protocol_id(&self) -> &ShapeId<'static>;
    fn serialize_request(
        &self,
        input: &dyn SerializableStruct,
        input_schema: &Schema<'_>,
        endpoint: &str,
        cfg: &ConfigBag,
    ) -> Result<Self::Request, SerdeError>;
    fn deserialize_response<'a>(
        &self,
        response: &'a Self::Response,
        output_schema: &Schema<'_>,
        cfg: &'a ConfigBag,
    ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError>;
    fn update_endpoint(
        &self,
        request: &mut Self::Request,
        endpoint: &Endpoint,
        cfg: &ConfigBag,
    ) -> Result<(), SerdeError>;

    // Provided methods
    fn parse_error_metadata(
        &self,
        response: &Self::Response,
        cfg: &ConfigBag,
    ) -> Result<ErrorMetadataBuilder, SerdeError> { ... }
    fn deserialize_error_response<'a>(
        &self,
        response: &'a Self::Response,
        cfg: &'a ConfigBag,
    ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> { ... }
    fn payload_codec(&self) -> Option<&dyn DynCodec> { ... }
    fn event_stream_media_type(&self) -> Option<&str> { ... }
    fn parse_event_stream_error_metadata(
        &self,
        payload: &[u8],
    ) -> Result<ErrorMetadataBuilder, SerdeError> { ... }
}
Expand description

Statically-dispatched client protocol trait — the one implementors write.

Request and Response are associated types so a protocol can target any transport (HTTP, MQTT, Unix-socket, in-memory, …). For the common HTTP case, set both to aws_smithy_runtime_api::http::Request / Response.

Callers who need to store a protocol behind dyn (e.g., in a ConfigBag for runtime swapping) should use the object-safe ClientProtocol trait instead. Every ClientProtocolInner is automatically a ClientProtocol<Self::Request, Self::Response> via a blanket impl, so implementors never write ClientProtocol manually.

See apply_http_endpoint for the canonical HTTP implementation of update_endpoint.

§Lifecycle

Instances are immutable and thread-safe. They are typically created once and shared across all requests for a client.

Required Associated Types§

Source

type Request

The protocol’s request message type (e.g., http::Request).

Source

type Response

The protocol’s response message type (e.g., http::Response).

Required Methods§

Source

fn protocol_id(&self) -> &ShapeId<'static>

Returns the Smithy shape ID of this protocol.

Source

fn serialize_request( &self, input: &dyn SerializableStruct, input_schema: &Schema<'_>, endpoint: &str, cfg: &ConfigBag, ) -> Result<Self::Request, SerdeError>

Serializes an operation input into a request message.

§The protocol owns its own wire format

Anything this protocol puts on the wire that the protocol alone determines must be resolved here, not emitted by codegen. A client generated for one protocol can be pointed at another with Config::builder().protocol(..), so anything codegen bakes in based on the generated protocol is wrong after a swap: left behind when this protocol is swapped out, and missing when it is swapped in. Concretely, an implementor owns:

  • Framing headers. rpcv2Cbor sets smithy-protocol and accept; awsJson sets X-Amz-Target. Codegen’s schema path deliberately emits none of these.
  • The request path, per the note on endpoint below.
  • Model facts the protocol needs but a caller cannot know, read from cfg: ServiceShapeName, ServiceVersion, ServiceXmlNamespace. Generated clients store these regardless of which protocol they were generated for, precisely so a swapped-in protocol can find them. Keep an explicit builder as an override for what the model cannot express – a target prefix that is not the service shape name, for instance.

Headers determined by the service rather than the protocol – x-amzn-query-mode, from @awsQueryCompatible – are outside this rule and stay in codegen, because their value does not change when the protocol does.

§endpoint is advisory

endpoint is a request path (or ""), never a host; scheme and authority are merged later by apply_http_endpoint. It is whatever codegen computed for the protocol the client was generated for, so a protocol that alone determines its route must ignore it:

  • Fixed route – awsJson and awsQuery are specified to POST /, so they pass / and ignore the argument entirely. Forwarding it would let an rpcv2Cbor-generated client POST to /service/{service}/operation/{operation}.
  • Route derived from model facts – rpcv2Cbor computes /service/{service}/operation/{operation} from cfg, falling back to endpoint only when those facts are absent.
  • Route from @http bindings – REST protocols expand the operation’s @http template from the schema, which is authoritative; endpoint is ignored. Generated REST clients pass "", so this costs them nothing, and it stops an RPC route from being prefixed onto the template after a swap. endpoint acts as the template only for a schema that carries no @http trait at all.

The shared HttpRpcProtocol helper deliberately does not hard-code /; only a concrete protocol knows whether its route is constant.

Source

fn deserialize_response<'a>( &self, response: &'a Self::Response, output_schema: &Schema<'_>, cfg: &'a ConfigBag, ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError>

Deserializes a response message, returning a boxed ShapeDeserializer that supplies every member of output_schema this protocol carries in the response.

The protocol, not the caller, decides where each member comes from. A protocol that uses HTTP bindings reads @httpHeader, @httpPrefixHeaders, @httpResponseCode and @httpPayload members from the HTTP message and the rest from the body (see http_output_deserializer); a body-only protocol ignores those traits and reads every member from the body. Generated code hands the result to the output builder’s member consumer and does no transport parsing itself.

A @streaming payload member is never supplied: the caller owns the live body.

cfg shares the returned deserializer’s lifetime so a protocol can defer reading response-scoped settings until they are needed; the HTTP-binding composite reads NonUtf8HeaderHandling only after a header fails to parse. An implementation that does not keep cfg may declare it as a plain &ConfigBag.

Source

fn update_endpoint( &self, request: &mut Self::Request, endpoint: &Endpoint, cfg: &ConfigBag, ) -> Result<(), SerdeError>

Updates a previously serialized request with a resolved endpoint.

Required by SEP requirement 7. The orchestrator calls this after endpoint resolution, which happens after serialize_request.

HTTP protocols should implement this as:

ⓘ
apply_http_endpoint(request, endpoint, cfg)

(See apply_http_endpoint.) Non-HTTP protocols implement the transport’s equivalent.

Provided Methods§

Source

fn parse_error_metadata( &self, response: &Self::Response, cfg: &ConfigBag, ) -> Result<ErrorMetadataBuilder, SerdeError>

Extracts canonical error metadata (code, message, request id) from a response’s wire envelope.

Returns a Builder so callers can attach per-request fields (e.g., x-amzn-RequestId from an HTTP header) before finalizing.

Concrete protocols override this to extract their envelope-specific fields:

  • awsJson1.0 / awsJson1.1: __type from the body, X-Amzn-Errortype header fallback.
  • restJson1: same as awsJson.
  • restXml (wrapped): <ErrorResponse><Error><Code> etc.
  • restXml (@restXml(noErrorWrapping: true)): <Error><Code> etc.
  • awsQuery / ec2Query: <ErrorResponse><Error><Code> etc.
  • rpcv2Cbor: __type from the CBOR map.

The default implementation returns an empty Builder — sufficient for protocols that haven’t migrated to schema-driven error dispatch yet, but callers will see Option::None for code() / message() and treat the response as an unhandled error.

Source

fn deserialize_error_response<'a>( &self, response: &'a Self::Response, cfg: &'a ConfigBag, ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError>

Returns a ShapeDeserializer positioned at the body of an error response — inside the protocol’s error envelope, where applicable.

Generated error dispatch code calls this to obtain a deserializer for the modeled error’s member consumer, regardless of which protocol is active at runtime. As with deserialize_response, the protocol decides which members come from the transport and which from the body.

For body-only protocols (awsJson1.0/1.1, awsQuery, ec2Query, rpcv2Cbor) the default implementation suffices: the body root is the error body, so it forwards to deserialize_response against prelude::DOCUMENT, and HTTP response bindings are correctly ignored.

Envelope-bearing protocols (restXml wrapped / unwrapped, awsQuery, ec2Query) MUST override to strip the outer <ErrorResponse> / <Error> wrapper before returning the deserializer.

§Protocols that use HTTP bindings must override this

A protocol whose errors carry @httpHeader, @httpPrefixHeaders or @httpResponseCode members MUST override this and build its deserializer with http_error_deserializer, even when its body needs no envelope positioning. The default’s two properties are both accidents rather than contracts: it evaluates the success body-only fast path, and it does so against a schema that is not the error’s. Neither is safe to depend on, because the concrete error schema is unknown until the generated error variant calls read_struct. Every built-in REST protocol therefore overrides this, and the_error_path_never_takes_the_body_only_fast_path pins the consequence.

Source

fn payload_codec(&self) -> Option<&dyn DynCodec>

Returns the codec used for payload (de)serialization, if any.

See DynCodec for why the codec is exposed through the object-safe sibling.

Source

fn event_stream_media_type(&self) -> Option<&str>

The media type used to label a structured event-stream payload, if this protocol supports event streams — for example application/cbor or application/json.

Every event-stream frame carries a :content-type header describing its payload. The payload itself is encoded by whichever protocol is selected at runtime, so this label has to come from that same protocol; a label baked in when the client was generated contradicts the bytes as soon as a different protocol is selected, and a peer that honours the header then decodes with the wrong codec.

§Why this is on the protocol and not on Codec

The value is protocol-determined, not format-determined, and one codec serves several protocols that disagree about it. A single JsonCodec backs restJson1, awsJson1_0 and awsJson1_1, whose request content types are application/json, application/x-amz-json-1.0 and application/x-amz-json-1.1 respectively. A codec-level accessor could therefore only ever return one of those, and would describe event payloads correctly only because the three JSON protocols happen to agree there.

§What this is not

This is not the request Content-Type. For awsJson the two differ, per the above. It is also not the content type of a payload whose type is fixed by the shape rather than the format: an @eventPayload blob is application/octet-stream and a string is text/plain under every protocol, so those stay with the code generator and must not be sourced from here.

Returns None by default, meaning the protocol declares no such media type — callers must keep a fallback. The default keeps this addition non-breaking for third-party protocols, which the SEP requires be able to exist without modifying a code generator.

Source

fn parse_event_stream_error_metadata( &self, payload: &[u8], ) -> Result<ErrorMetadataBuilder, SerdeError>

Extracts canonical error metadata from the payload of an event-stream exception frame.

This is the event-stream counterpart of parse_error_metadata, and it exists separately for a typing reason rather than a behavioral one: that method takes &Self::Response, an HTTP response, and an event-stream frame is not one. The parsing itself is identical — both read a protocol-specific error envelope out of a byte payload — so implementors should delegate to the same helper they use there.

§Why this must be resolved by the protocol

The frame’s payload is encoded by whichever protocol is selected at runtime (via payload_codec), so its error envelope must be parsed by that same protocol. A code generator cannot decide this: it knows only the protocol the client was generated for, and after a runtime protocol swap that is the wrong one. Getting it wrong is not silent — the parse fails and the caller reports an unhandled error — but it costs the error code, and with it the modeled error variant and any retry classification keyed on that code.

§Scope

Takes the payload only. An event-stream frame carries no HTTP headers, so there is nothing to pass for the header-borne discriminators some protocols also accept (restJson1’s x-amzn-errortype, awsQuery-compatible’s x-amzn-query-error); implementors should parse as though the header map were empty. The frame’s own :exception-type header is a separate mechanism and is handled by generated dispatch code before this is called.

The default returns an empty Builder, matching parse_error_metadata: callers see Option::None for code() / message() and treat the frame as an unhandled error. The default keeps this addition non-breaking for third-party protocols, which the SEP requires be able to exist without modifying a code generator.

Implementors of a protocol without event streams should leave this alone.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§