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.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 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 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 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 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(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}