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.component_id())))]
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 component = route_match.component_id();
31
32        tracing::trace!(
33            "Executing request using the Wagi executor for component {}",
34            component
35        );
36
37        let uri_path = req.uri().path();
38
39        // Build the argv array by starting with the config for `argv` and substituting in
40        // script name and args where appropriate.
41        let script_name = uri_path.to_string();
42        let args = req.uri().query().unwrap_or_default().replace('&', " ");
43        let argv = self
44            .wagi_config
45            .argv
46            .clone()
47            .replace("${SCRIPT_NAME}", &script_name)
48            .replace("${ARGS}", &args);
49
50        let (parts, body) = req.into_parts();
51
52        let body = body.collect().await?.to_bytes().to_vec();
53        let len = body.len();
54
55        // TODO
56        // The default host and TLS fields are currently hard-coded.
57        let mut headers =
58            wagi::build_headers(route_match, &parts, len, client_addr, "default_host", false);
59
60        let default_host = http::HeaderValue::from_str("localhost")?;
61        let host = std::str::from_utf8(
62            parts
63                .headers
64                .get("host")
65                .unwrap_or(&default_host)
66                .as_bytes(),
67        )?;
68
69        // Add the default Spin headers.
70        // This sets the current environment variables Wagi expects (such as
71        // `PATH_INFO`, or `X_FULL_URL`).
72        // Note that this overrides any existing headers previously set by Wagi.
73        for (keys, val) in compute_default_headers(&parts.uri, host, route_match, client_addr)? {
74            headers.insert(keys[1].to_string(), val.into_owned());
75        }
76
77        let stdout = MemoryOutputPipe::new(usize::MAX);
78
79        let wasi_builder = instance_builder
80            .factor_builder::<WasiFactor>()
81            .context("The wagi HTTP trigger was configured without the required wasi support")?;
82
83        // Set up Wagi environment
84        wasi_builder.args(argv.split(' '));
85        wasi_builder.env(headers);
86        wasi_builder.stdin_pipe(Cursor::new(body));
87        wasi_builder.stdout(stdout.clone());
88
89        let (instance, mut store) = instance_builder.instantiate(()).await?;
90
91        let command = self.indices.load(&mut store, &instance)?;
92
93        tracing::trace!("Calling Wasm entry point");
94        if let Err(()) = command
95            .wasi_cli_run()
96            .call_run(&mut store)
97            .await
98            .or_else(ignore_successful_proc_exit_trap)?
99        {
100            tracing::error!("Wagi main function returned unsuccessful result");
101        }
102        tracing::info!("Wagi execution complete");
103
104        // Drop the store so we're left with a unique reference to `stdout`:
105        drop(store);
106
107        let stdout = stdout.try_into_inner().unwrap();
108        ensure!(
109            !stdout.is_empty(),
110            "The {component:?} component is configured to use the WAGI executor \
111             but did not write to stdout. Check the `executor` in spin.toml."
112        );
113
114        wagi::compose_response(&stdout)
115    }
116}
117
118fn ignore_successful_proc_exit_trap(guest_err: anyhow::Error) -> Result<Result<(), ()>> {
119    match guest_err
120        .root_cause()
121        .downcast_ref::<wasmtime_wasi::I32Exit>()
122    {
123        Some(trap) => match trap.0 {
124            0 => Ok(Ok(())),
125            _ => Err(guest_err),
126        },
127        None => Err(guest_err),
128    }
129}