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_02x::header::HeaderName;
17use http_02x::uri::PathAndQuery;
18use http_02x::{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    apply_endpoint_to_request(request, endpoint, endpoint_prefix)
108}
109
110fn apply_endpoint_to_request(
111    request: &mut HttpRequest,
112    endpoint: &Endpoint,
113    endpoint_prefix: Option<&EndpointPrefix>,
114) -> Result<(), BoxError> {
115    let endpoint_url = match endpoint_prefix {
116        None => Cow::Borrowed(endpoint.url()),
117        Some(prefix) => {
118            let parsed = endpoint.url().parse::<Uri>()?;
119            let scheme = parsed.scheme_str().unwrap_or_default();
120            let prefix = prefix.as_str();
121            let authority = parsed
122                .authority()
123                .map(|auth| auth.as_str())
124                .unwrap_or_default();
125            let path_and_query = parsed
126                .path_and_query()
127                .map(PathAndQuery::as_str)
128                .unwrap_or_default();
129            Cow::Owned(format!("{scheme}://{prefix}{authority}{path_and_query}"))
130        }
131    };
132
133    request
134        .uri_mut()
135        .set_endpoint(&endpoint_url)
136        .map_err(|err| {
137            ResolveEndpointError::message(format!(
138                "failed to apply endpoint `{}` to request `{:?}`",
139                endpoint_url, request,
140            ))
141            .with_source(Some(err.into()))
142        })?;
143
144    for (header_name, header_values) in endpoint.headers() {
145        request.headers_mut().remove(header_name);
146        for value in header_values {
147            request.headers_mut().append(
148                HeaderName::from_str(header_name).map_err(|err| {
149                    ResolveEndpointError::message("invalid header name")
150                        .with_source(Some(err.into()))
151                })?,
152                HeaderValue::from_str(value).map_err(|err| {
153                    ResolveEndpointError::message("invalid header value")
154                        .with_source(Some(err.into()))
155                })?,
156            );
157        }
158    }
159    Ok(())
160}
161
162#[cfg(test)]
163mod test {
164    use aws_smithy_runtime_api::client::endpoint::EndpointPrefix;
165    use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
166    use aws_smithy_types::endpoint::Endpoint;
167
168    #[test]
169    fn test_apply_endpoint() {
170        let mut req = HttpRequest::empty();
171        req.set_uri("/foo?bar=1").unwrap();
172        let endpoint = Endpoint::builder().url("https://s3.amazon.com").build();
173        let prefix = EndpointPrefix::new("prefix.subdomain.").unwrap();
174        super::apply_endpoint_to_request(&mut req, &endpoint, Some(&prefix))
175            .expect("should succeed");
176        assert_eq!(
177            req.uri(),
178            "https://prefix.subdomain.s3.amazon.com/foo?bar=1"
179        );
180    }
181}