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            .property("is_custom_endpoint", true)
51            .build()))
52    }
53}
54
55/// Empty params to be used with [`StaticUriEndpointResolver`].
56#[derive(Debug, Default)]
57pub struct StaticUriEndpointResolverParams;
58
59impl StaticUriEndpointResolverParams {
60    /// Creates a new `StaticUriEndpointResolverParams`.
61    pub fn new() -> Self {
62        Self
63    }
64}
65
66impl From<StaticUriEndpointResolverParams> for EndpointResolverParams {
67    fn from(params: StaticUriEndpointResolverParams) -> Self {
68        EndpointResolverParams::new(params)
69    }
70}
71
72pub(super) async fn orchestrate_endpoint(
73    identity: Identity,
74    ctx: &mut InterceptorContext,
75    runtime_components: &RuntimeComponents,
76    cfg: &mut ConfigBag,
77) -> Result<(), BoxError> {
78    trace!("orchestrating endpoint resolution");
79
80    let params = cfg
81        .get_mut_from_interceptor_state::<EndpointResolverParams>()
82        .expect("endpoint resolver params must be set in the interceptor state");
83    params.set_property(identity);
84
85    let endpoint_resolver = runtime_components.endpoint_resolver();
86
87    endpoint_resolver.finalize_params(params)?;
88
89    tracing::debug!(endpoint_params = ?params, "resolving endpoint");
90    let endpoint = endpoint_resolver.resolve_endpoint(params).await?;
91
92    apply_endpoint(&endpoint, ctx, cfg)?;
93
94    // Make the endpoint config available to interceptors
95    cfg.interceptor_state().store_put(endpoint);
96    Ok(())
97}
98
99pub(super) fn apply_endpoint(
100    endpoint: &Endpoint,
101    ctx: &mut InterceptorContext,
102    cfg: &ConfigBag,
103) -> Result<(), BoxError> {
104    let endpoint_prefix = cfg.load::<EndpointPrefix>();
105    tracing::debug!(endpoint_prefix = ?endpoint_prefix, "will apply endpoint {:?}", endpoint);
106    let request = ctx.request_mut().expect("set during serialization");
107
108    apply_endpoint_to_request(request, endpoint, endpoint_prefix)
109}
110
111fn apply_endpoint_to_request(
112    request: &mut HttpRequest,
113    endpoint: &Endpoint,
114    endpoint_prefix: Option<&EndpointPrefix>,
115) -> Result<(), BoxError> {
116    let endpoint_url = match endpoint_prefix {
117        None => Cow::Borrowed(endpoint.url()),
118        Some(prefix) => {
119            let parsed = endpoint.url().parse::<Uri>()?;
120            let scheme = parsed.scheme_str().unwrap_or_default();
121            let prefix = prefix.as_str();
122            let authority = parsed
123                .authority()
124                .map(|auth| auth.as_str())
125                .unwrap_or_default();
126            let path_and_query = parsed
127                .path_and_query()
128                .map(PathAndQuery::as_str)
129                .unwrap_or_default();
130            Cow::Owned(format!("{scheme}://{prefix}{authority}{path_and_query}"))
131        }
132    };
133
134    request
135        .uri_mut()
136        .set_endpoint(&endpoint_url)
137        .map_err(|err| {
138            ResolveEndpointError::message(format!(
139                "failed to apply endpoint `{endpoint_url}` to request `{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}