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