spin_trigger_http/
wagi.rs

1use std::{io::Cursor, net::SocketAddr};
2
3use anyhow::{ensure, Context, Result};
4use http_body_util::BodyExt;
5use hyper::{Request, Response};
6use spin_factor_wasi::WasiFactor;
7use spin_factors::RuntimeFactors;
8use spin_http::{config::WagiTriggerConfig, routes::RouteMatch, wagi};
9use tracing::{instrument, Level};
10use wasmtime_wasi::p2::bindings::CommandIndices;
11use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
12use wasmtime_wasi_http::body::HyperIncomingBody as Body;
13
14use crate::{headers::compute_default_headers, server::HttpExecutor, TriggerInstanceBuilder};
15
16pub struct WagiHttpExecutor<'a> {
17    pub wagi_config: &'a WagiTriggerConfig,
18    pub indices: &'a CommandIndices,
19}
20
21impl HttpExecutor for WagiHttpExecutor<'_> {
22    #[instrument(name = "spin_trigger_http.execute_wagi", skip_all, err(level = Level::INFO), fields(otel.name = format!("execute_wagi_component {}", route_match.lookup_key().to_string())))]
23    async fn execute<F: RuntimeFactors>(
24        &self,
25        mut instance_builder: TriggerInstanceBuilder<'_, F>,
26        route_match: &RouteMatch<'_, '_>,
27        req: Request<Body>,
28        client_addr: SocketAddr,
29    ) -> Result<Response<Body>> {
30        let spin_http::routes::TriggerLookupKey::Component(component) = route_match.lookup_key()
31        else {
32            anyhow::bail!("INCONCEIVABLE");
33        };
34
35        tracing::trace!(
36            "Executing request using the Wagi executor for component {}",
37            component
38        );
39
40        let uri_path = req.uri().path();
41
42        // Build the argv array by starting with the config for `argv` and substituting in
43        // script name and args where appropriate.
44        let script_name = uri_path.to_string();
45        let args = req.uri().query().unwrap_or_default().replace('&', " ");
46        let argv = self
47            .wagi_config
48            .argv
49            .clone()
50            .replace("${SCRIPT_NAME}", &script_name)
51            .replace("${ARGS}", &args);
52
53        let (parts, body) = req.into_parts();
54
55        let body = body.collect().await?.to_bytes().to_vec();
56        let len = body.len();
57
58        // TODO
59        // The default host and TLS fields are currently hard-coded.
60        let mut headers =
61            wagi::build_headers(route_match, &parts, len, client_addr, "default_host", false);
62
63        let default_host = http::HeaderValue::from_str("localhost")?;
64        let host = std::str::from_utf8(
65            parts
66                .headers
67                .get("host")
68                .unwrap_or(&default_host)
69                .as_bytes(),
70        )?;
71
72        // Add the default Spin headers.
73        // This sets the current environment variables Wagi expects (such as
74        // `PATH_INFO`, or `X_FULL_URL`).
75        // Note that this overrides any existing headers previously set by Wagi.
76        for (keys, val) in compute_default_headers(&parts.uri, host, route_match, client_addr)? {
77            headers.insert(keys[1].to_string(), val.into_owned());
78        }
79
80        let stdout = MemoryOutputPipe::new(usize::MAX);
81
82        let wasi_builder = instance_builder
83            .factor_builder::<WasiFactor>()
84            .context("The wagi HTTP trigger was configured without the required wasi support")?;
85
86        // Set up Wagi environment
87        wasi_builder.args(argv.split(' '));
88        wasi_builder.env(headers);
89        wasi_builder.stdin_pipe(Cursor::new(body));
90        wasi_builder.stdout(stdout.clone());
91
92        let (instance, mut store) = instance_builder.instantiate(()).await?;
93
94        let command = self.indices.load(&mut store, &instance)?;
95
96        tracing::trace!("Calling Wasm entry point");
97        if let Err(()) = command
98            .wasi_cli_run()
99            .call_run(&mut store)
100            .await
101            .or_else(ignore_successful_proc_exit_trap)?
102        {
103            tracing::error!("Wagi main function returned unsuccessful result");
104        }
105        tracing::info!("Wagi execution complete");
106
107        // Drop the store so we're left with a unique reference to `stdout`:
108        drop(store);
109
110        let stdout = stdout.try_into_inner().unwrap();
111        ensure!(
112            !stdout.is_empty(),
113            "The {component:?} component is configured to use the WAGI executor \
114             but did not write to stdout. Check the `executor` in spin.toml."
115        );
116
117        wagi::compose_response(&stdout)
118    }
119}
120
121fn ignore_successful_proc_exit_trap(guest_err: anyhow::Error) -> Result<Result<(), ()>> {
122    match guest_err
123        .root_cause()
124        .downcast_ref::<wasmtime_wasi::I32Exit>()
125    {
126        Some(trap) => match trap.0 {
127            0 => Ok(Ok(())),
128            _ => Err(guest_err),
129        },
130        None => Err(guest_err),
131    }
132}