spin_trigger_http/
wagi.rs1use 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 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 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 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 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(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}