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