aws_smithy_wasm/
wasi.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! WASI HTTP Adapter
7use aws_smithy_async::rt::sleep::{AsyncSleep, Sleep};
8use aws_smithy_runtime_api::client::connector_metadata::ConnectorMetadata;
9use aws_smithy_runtime_api::{
10    client::{
11        http::{
12            HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings,
13            SharedHttpClient, SharedHttpConnector,
14        },
15        orchestrator::HttpRequest,
16        result::ConnectorError,
17        runtime_components::RuntimeComponents,
18    },
19    http::Response,
20    shared::IntoShared,
21};
22use aws_smithy_types::{body::SdkBody, retry::ErrorKind};
23use http_body_util::{BodyStream, StreamBody};
24use std::time::Duration;
25use wstd::http::{Body as WstdBody, BodyExt as _, Client, Error as WstdError};
26
27/// An sleep implementation for wasip2, using the wstd async executor.
28#[derive(Debug, Clone)]
29pub struct WasiSleep;
30impl AsyncSleep for WasiSleep {
31    fn sleep(&self, duration: Duration) -> Sleep {
32        Sleep::new(async move {
33            wstd::task::sleep(wstd::time::Duration::from(duration)).await;
34        })
35    }
36}
37
38/// Builder for [`WasiHttpClient`]. Currently empty, but allows for future
39/// config options to be added in a backwards compatible manner.
40#[derive(Default, Debug)]
41#[non_exhaustive]
42pub struct WasiHttpClientBuilder {}
43
44impl WasiHttpClientBuilder {
45    /// Creates a new builder.
46    pub fn new() -> Self {
47        Default::default()
48    }
49
50    /// Builds the [`WasiHttpClient`].
51    pub fn build(self) -> SharedHttpClient {
52        let client = WasiHttpClient {};
53        client.into_shared()
54    }
55}
56
57/// An HTTP client that can be used during instantiation of the client SDK in
58/// order to route the HTTP requests through the WebAssembly host. The host must
59/// support the wasi-http interface as defined in the WASIp2 specification.
60#[derive(Debug, Clone)]
61#[non_exhaustive]
62pub struct WasiHttpClient {}
63
64impl HttpClient for WasiHttpClient {
65    fn http_connector(
66        &self,
67        settings: &HttpConnectorSettings,
68        _components: &RuntimeComponents,
69    ) -> SharedHttpConnector {
70        let mut client = Client::new();
71        if let Some(timeout) = settings.connect_timeout() {
72            client.set_connect_timeout(timeout);
73        }
74        if let Some(timeout) = settings.read_timeout() {
75            client.set_first_byte_timeout(timeout);
76        }
77        SharedHttpConnector::new(WasiHttpConnector(client))
78    }
79
80    fn connector_metadata(&self) -> Option<ConnectorMetadata> {
81        Some(ConnectorMetadata::new("wasi-http-client", None))
82    }
83}
84
85/// HTTP connector used in WASI environment
86#[derive(Debug, Clone)]
87struct WasiHttpConnector(Client);
88
89impl HttpConnector for WasiHttpConnector {
90    fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
91        let client = self.0.clone();
92        HttpConnectorFuture::new(async move {
93            let request = request
94                .try_into_http1x()
95                // This can only fail if the Extensions fail to convert
96                .map_err(|e| ConnectorError::other(Box::new(e), None))?;
97            // smithy's SdkBody Error is a non-'static boxed dyn stderror.
98            // Anyhow can't represent that, so convert it to the debug impl.
99            let request = request.map(|body| {
100                WstdBody::from_http_body(body.map_err(|e| WstdError::msg(format!("{e:?}"))))
101            });
102            // Any error given by send is considered a "ClientError" kind
103            // which should prevent smithy from retrying like it would for a
104            // throttling error
105            let response = client
106                .send(request)
107                .await
108                .map_err(|e| ConnectorError::other(e.into(), Some(ErrorKind::ClientError)))?;
109
110            Response::try_from(response.map(|wstd_body| {
111                // You'd think that an SdkBody would just be an impl Body with
112                // the usual error type dance.
113                let nonsync_body = wstd_body
114                    .into_boxed_body()
115                    .map_err(|e| e.into_boxed_dyn_error());
116                // But we have to do this weird dance: because Axum insists
117                // bodies are not Sync, wstd settled on non-Sync bodies.
118                // Smithy insists on Sync bodies. The SyncStream type exists
119                // to assert, because all Stream operations are on &mut self,
120                // all Streams are Sync. So, turn the Body into a Stream, make
121                // it sync, then back to a Body.
122                let nonsync_stream = BodyStream::new(nonsync_body);
123                let sync_stream = sync_wrapper::SyncStream::new(nonsync_stream);
124                let sync_body = StreamBody::new(sync_stream);
125                SdkBody::from_body_1_x(sync_body)
126            }))
127            // This can only fail if the Extensions fail to convert
128            .map_err(|e| ConnectorError::other(Box::new(e), None))
129        })
130    }
131}