Skip to main content

spin_trigger_http/
wagi.rs

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