aws_smithy_runtime/client/orchestrator/
endpoints.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use aws_smithy_runtime_api::client::endpoint::{
7    error::ResolveEndpointError, EndpointFuture, EndpointResolverParams, ResolveEndpoint,
8};
9use aws_smithy_runtime_api::client::identity::Identity;
10use aws_smithy_runtime_api::client::interceptors::context::InterceptorContext;
11use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
12use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
13use aws_smithy_runtime_api::{box_error::BoxError, client::endpoint::EndpointPrefix};
14use aws_smithy_types::config_bag::ConfigBag;
15use aws_smithy_types::endpoint::Endpoint;
16use http_1x::header::HeaderName;
17use http_1x::uri::PathAndQuery;
18use http_1x::{HeaderValue, Uri};
19use std::borrow::Cow;
20use std::fmt::Debug;
21use std::str::FromStr;
22use tracing::trace;
23
24/// An endpoint resolver that uses a static URI.
25#[derive(Clone, Debug)]
26pub struct StaticUriEndpointResolver {
27    endpoint: String,
28}
29
30impl StaticUriEndpointResolver {
31    /// Create a resolver that resolves to `http://localhost:{port}`.
32    pub fn http_localhost(port: u16) -> Self {
33        Self {
34            endpoint: format!("http://localhost:{port}"),
35        }
36    }
37
38    /// Create a resolver that resolves to the given URI.
39    pub fn uri(endpoint: impl Into<String>) -> Self {
40        Self {
41            endpoint: endpoint.into(),
42        }
43    }
44}
45
46impl ResolveEndpoint for StaticUriEndpointResolver {
47    fn resolve_endpoint<'a>(&'a self, _params: &'a EndpointResolverParams) -> EndpointFuture<'a> {
48        EndpointFuture::ready(Ok(Endpoint::builder()
49            .url(self.endpoint.to_string())
50            .build()))
51    }
52}
53
54/// Empty params to be used with [`StaticUriEndpointResolver`].
55#[derive(Debug, Default)]
56pub struct StaticUriEndpointResolverParams;
57
58impl StaticUriEndpointResolverParams {
59    /// Creates a new `StaticUriEndpointResolverParams`.
60    pub fn new() -> Self {
61        Self
62    }
63}
64
65impl From<StaticUriEndpointResolverParams> for EndpointResolverParams {
66    fn from(params: StaticUriEndpointResolverParams) -> Self {
67        EndpointResolverParams::new(params)
68    }
69}
70
71pub(super) async fn orchestrate_endpoint(
72    identity: Identity,
73    ctx: &mut InterceptorContext,
74    runtime_components: &RuntimeComponents,
75    cfg: &mut ConfigBag,
76) -> Result<(), BoxError> {
77    trace!("orchestrating endpoint resolution");
78
79    let params = cfg
80        .get_mut_from_interceptor_state::<EndpointResolverParams>()
81        .expect("endpoint resolver params must be set in the interceptor state");
82    params.set_property(identity);
83
84    let endpoint_resolver = runtime_components.endpoint_resolver();
85
86    endpoint_resolver.finalize_params(params)?;
87
88    tracing::debug!(endpoint_params = ?params, "resolving endpoint");
89    let endpoint = endpoint_resolver.resolve_endpoint(params).await?;
90
91    apply_endpoint(&endpoint, ctx, cfg)?;
92
93    // Make the endpoint config available to interceptors
94    cfg.interceptor_state().store_put(endpoint);
95    Ok(())
96}
97
98pub(super) fn apply_endpoint(
99    endpoint: &Endpoint,
100    ctx: &mut InterceptorContext,
101    cfg: &ConfigBag,
102) -> Result<(), BoxError> {
103    let endpoint_prefix = cfg.load::<EndpointPrefix>();
104    tracing::debug!(endpoint_prefix = ?endpoint_prefix, "will apply endpoint {:?}", endpoint);
105    let request = ctx.request_mut().expect("set during serialization");
106
107    // If a schema-driven protocol is in use, delegate endpoint application to it.
108    if let Some(protocol) = cfg.load::<aws_smithy_schema::protocol::SharedClientProtocol>() {
109        protocol
110            .update_endpoint(request, endpoint, cfg)
111            .map_err(BoxError::from)?;
112    } else {
113        apply_endpoint_to_request(request, endpoint, endpoint_prefix)?;
114    }
115
116    Ok(())
117}
118
119fn apply_endpoint_to_request(
120    request: &mut HttpRequest,
121    endpoint: &Endpoint,
122    endpoint_prefix: Option<&EndpointPrefix>,
123) -> Result<(), BoxError> {
124    let endpoint_url = match endpoint_prefix {
125        None => Cow::Borrowed(endpoint.url()),
126        Some(prefix) => {
127            let parsed = endpoint.url().parse::<Uri>()?;
128            let scheme = parsed.scheme_str().unwrap_or_default();
129            let prefix = prefix.as_str();
130            let authority = parsed
131                .authority()
132                .map(|auth| auth.as_str())
133                .unwrap_or_default();
134            let path_and_query = parsed
135                .path_and_query()
136                .map(PathAndQuery::as_str)
137                .unwrap_or_default();
138            Cow::Owned(format!("{scheme}://{prefix}{authority}{path_and_query}"))
139        }
140    };
141
142    request
143        .uri_mut()
144        .set_endpoint(&endpoint_url)
145        .map_err(|err| {
146            ResolveEndpointError::message(format!(
147                "failed to apply endpoint `{endpoint_url}` to request `{request:?}`",
148            ))
149            .with_source(Some(err.into()))
150        })?;
151
152    for (header_name, header_values) in endpoint.headers() {
153        request.headers_mut().remove(header_name);
154        for value in header_values {
155            request.headers_mut().append(
156                HeaderName::from_str(header_name).map_err(|err| {
157                    ResolveEndpointError::message("invalid header name")
158                        .with_source(Some(err.into()))
159                })?,
160                HeaderValue::from_str(value).map_err(|err| {
161                    ResolveEndpointError::message("invalid header value")
162                        .with_source(Some(err.into()))
163                })?,
164            );
165        }
166    }
167    Ok(())
168}
169
170#[cfg(test)]
171mod test {
172    use aws_smithy_runtime_api::client::endpoint::EndpointPrefix;
173    use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
174    use aws_smithy_types::endpoint::Endpoint;
175
176    #[test]
177    fn test_apply_endpoint() {
178        let mut req = HttpRequest::empty();
179        req.set_uri("/foo?bar=1").unwrap();
180        let endpoint = Endpoint::builder().url("https://s3.amazon.com").build();
181        let prefix = EndpointPrefix::new("prefix.subdomain.").unwrap();
182        super::apply_endpoint_to_request(&mut req, &endpoint, Some(&prefix))
183            .expect("should succeed");
184        assert_eq!(
185            req.uri(),
186            "https://prefix.subdomain.s3.amazon.com/foo?bar=1"
187        );
188    }
189}