1mod headers;
4mod instrument;
5mod middleware;
6mod outbound_http;
7mod server;
8mod spin;
9mod tls;
10mod wagi;
11mod wasi;
12mod wasip3;
13
14use std::{
15 error::Error,
16 fmt::Display,
17 net::{Ipv4Addr, SocketAddr, ToSocketAddrs},
18 path::PathBuf,
19 str::FromStr,
20 sync::Arc,
21 time::Duration,
22};
23
24use anyhow::{Context, bail};
25use clap::Args;
26use rand::{
27 distr::uniform::{SampleRange, SampleUniform},
28 rand_core::Rng,
29};
30use serde::Deserialize;
31use spin_app::App;
32use spin_factors::RuntimeFactors;
33use spin_trigger::Trigger;
34use wasmtime_wasi_http::p2::bindings::http::types::ErrorCode;
35
36pub use server::HttpServer;
37
38pub use tls::TlsConfig;
39
40pub(crate) use wasmtime_wasi_http::p2::body::HyperIncomingBody as Body;
41
42const DEFAULT_WASIP3_MAX_INSTANCE_REUSE_COUNT: usize = 128;
43const DEFAULT_WASIP3_MAX_INSTANCE_CONCURRENT_REUSE_COUNT: usize = 16;
44const DEFAULT_REQUEST_TIMEOUT: Option<Range<Duration>> = None;
45const DEFAULT_IDLE_INSTANCE_TIMEOUT: Range<Duration> = Range::Value(Duration::from_secs(1));
46
47#[derive(clap::ValueEnum, Clone, Copy, Debug, Default)]
49pub enum OutputFormat {
50 #[default]
52 Plain,
53 Json,
55}
56
57pub(crate) type TriggerApp<F> = spin_trigger::TriggerApp<HttpTrigger, F>;
59
60pub(crate) type TriggerInstanceBuilder<'a, F> =
62 spin_trigger::TriggerInstanceBuilder<'a, HttpTrigger, F>;
63
64#[derive(Args)]
65pub struct CliArgs {
66 #[clap(long = "listen", env = "SPIN_HTTP_LISTEN_ADDR", default_value = "127.0.0.1:3000", value_parser = parse_listen_addr)]
68 pub address: SocketAddr,
69
70 #[clap(long, env = "SPIN_TLS_CERT", requires = "tls_key")]
72 pub tls_cert: Option<PathBuf>,
73
74 #[clap(long, env = "SPIN_TLS_KEY", requires = "tls_cert")]
76 pub tls_key: Option<PathBuf>,
77
78 #[clap(long, env = "SPIN_HTTP1_MAX_BUF_SIZE")]
80 pub http1_max_buf_size: Option<usize>,
81
82 #[clap(long = "find-free-port")]
83 pub find_free_port: bool,
84
85 #[clap(value_enum, long = "format", default_value_t = OutputFormat::default())]
86 pub format: OutputFormat,
87
88 #[clap(long, value_parser = parse_usize_range)]
99 pub max_instance_reuse_count: Option<Range<usize>>,
100
101 #[clap(long, value_parser = parse_usize_range)]
112 pub max_instance_concurrent_reuse_count: Option<Range<usize>>,
113
114 #[clap(long, value_parser = parse_duration_range)]
126 pub request_timeout: Option<Range<Duration>>,
127
128 #[clap(long, default_value = "1s", value_parser = parse_duration_range)]
139 pub idle_instance_timeout: Range<Duration>,
140}
141
142impl CliArgs {
143 fn into_tls_config(self) -> Option<TlsConfig> {
144 match (self.tls_cert, self.tls_key) {
145 (Some(cert_path), Some(key_path)) => Some(TlsConfig {
146 cert_path,
147 key_path,
148 }),
149 (None, None) => None,
150 _ => unreachable!(),
151 }
152 }
153}
154
155#[derive(Copy, Clone)]
156pub enum Range<T> {
157 Value(T),
158 Bounds(T, T),
159}
160
161impl<T> Range<T> {
162 fn map<V>(self, fun: impl Fn(T) -> V) -> Range<V> {
163 match self {
164 Self::Value(v) => Range::Value(fun(v)),
165 Self::Bounds(a, b) => Range::Bounds(fun(a), fun(b)),
166 }
167 }
168}
169
170impl<T: SampleUniform + PartialOrd> SampleRange<T> for Range<T> {
171 fn sample_single<R: Rng + ?Sized>(self, rng: &mut R) -> Result<T, rand::distr::uniform::Error> {
172 match self {
173 Self::Value(v) => Ok(v),
174 Self::Bounds(a, b) => (a..b).sample_single(rng),
175 }
176 }
177
178 fn is_empty(&self) -> bool {
179 match self {
180 Self::Value(_) => false,
181 Self::Bounds(a, b) => (a..b).is_empty(),
182 }
183 }
184}
185
186fn parse_range<T: FromStr>(s: &str) -> Result<Range<T>, String>
187where
188 T::Err: Display,
189{
190 let error = |e| format!("expected integer or range; got {s:?}; {e}");
191 if let Some((start, end)) = s.split_once("..") {
192 Ok(Range::Bounds(
193 start.parse().map_err(error)?,
194 end.parse().map_err(error)?,
195 ))
196 } else {
197 Ok(Range::Value(s.parse().map_err(error)?))
198 }
199}
200
201fn parse_usize_range(s: &str) -> Result<Range<usize>, String> {
202 parse_range(s)
203}
204
205struct ParsedDuration(Duration);
206
207impl FromStr for ParsedDuration {
208 type Err = String;
209
210 fn from_str(s: &str) -> Result<Self, Self::Err> {
211 let error = |e| {
212 format!("expected integer suffixed by `s`, `ms`, `us`, `μs`, or `ns`; got {s:?}; {e}")
213 };
214 Ok(Self(match s.parse() {
215 Ok(val) => Duration::from_secs(val),
216 Err(err) => {
217 if let Some(num) = s.strip_suffix("ms") {
218 Duration::from_millis(num.parse().map_err(error)?)
219 } else if let Some(num) = s.strip_suffix("us").or(s.strip_suffix("μs")) {
220 Duration::from_micros(num.parse().map_err(error)?)
221 } else if let Some(num) = s.strip_suffix("ns") {
222 Duration::from_nanos(num.parse().map_err(error)?)
223 } else if let Some(num) = s.strip_suffix("s") {
224 Duration::from_secs(num.parse().map_err(error)?)
225 } else {
226 return Err(error(err));
227 }
228 }
229 }))
230 }
231}
232
233fn parse_duration_range(s: &str) -> Result<Range<Duration>, String> {
234 parse_range::<ParsedDuration>(s).map(|v| v.map(|v| v.0))
235}
236
237#[derive(Clone, Copy)]
238pub struct InstanceReuseConfig {
239 max_instance_reuse_count: Range<usize>,
240 max_instance_concurrent_reuse_count: Range<usize>,
241 request_timeout: Option<Range<Duration>>,
242 request_deadline: Option<Duration>,
243 idle_instance_timeout: Range<Duration>,
244}
245
246impl Default for InstanceReuseConfig {
247 fn default() -> Self {
248 Self {
249 max_instance_reuse_count: Range::Value(DEFAULT_WASIP3_MAX_INSTANCE_REUSE_COUNT),
250 max_instance_concurrent_reuse_count: Range::Value(
251 DEFAULT_WASIP3_MAX_INSTANCE_CONCURRENT_REUSE_COUNT,
252 ),
253 request_timeout: DEFAULT_REQUEST_TIMEOUT,
254 request_deadline: None,
255 idle_instance_timeout: DEFAULT_IDLE_INSTANCE_TIMEOUT,
256 }
257 }
258}
259
260impl InstanceReuseConfig {
261 pub fn single_use_with_request_deadline(timeout: Duration) -> Self {
269 Self {
270 max_instance_reuse_count: Range::Value(1),
271 max_instance_concurrent_reuse_count: Range::Value(1),
272 request_timeout: Some(Range::Value(timeout)),
273 request_deadline: Some(timeout),
274 idle_instance_timeout: DEFAULT_IDLE_INSTANCE_TIMEOUT,
275 }
276 }
277}
278
279pub struct HttpTrigger {
281 listen_addr: SocketAddr,
286 tls_config: Option<TlsConfig>,
287 find_free_port: bool,
288 http1_max_buf_size: Option<usize>,
289 reuse_config: InstanceReuseConfig,
290 output_format: OutputFormat,
291}
292
293impl<F: RuntimeFactors> Trigger<F> for HttpTrigger {
294 const TYPE: &'static str = "http";
295
296 type CliArgs = CliArgs;
297 type InstanceState = ();
298
299 fn new(cli_args: Self::CliArgs, app: &spin_app::App) -> anyhow::Result<Self> {
300 let find_free_port = cli_args.find_free_port;
301 let http1_max_buf_size = cli_args.http1_max_buf_size;
302 let output_format = cli_args.format;
303 let reuse_config = InstanceReuseConfig {
304 max_instance_reuse_count: cli_args
305 .max_instance_reuse_count
306 .unwrap_or(Range::Value(DEFAULT_WASIP3_MAX_INSTANCE_REUSE_COUNT)),
307 max_instance_concurrent_reuse_count: cli_args
308 .max_instance_concurrent_reuse_count
309 .unwrap_or(Range::Value(
310 DEFAULT_WASIP3_MAX_INSTANCE_CONCURRENT_REUSE_COUNT,
311 )),
312 request_timeout: cli_args.request_timeout,
313 request_deadline: None,
314 idle_instance_timeout: cli_args.idle_instance_timeout,
315 };
316
317 Self::new(
318 app,
319 cli_args.address,
320 cli_args.into_tls_config(),
321 find_free_port,
322 http1_max_buf_size,
323 reuse_config,
324 output_format,
325 )
326 }
327
328 async fn run(self, trigger_app: TriggerApp<F>) -> anyhow::Result<()> {
329 let server = self.into_server(trigger_app)?;
330
331 server.serve().await?;
332
333 Ok(())
334 }
335
336 fn trigger_dependencies_composer() -> impl spin_factors_executor::TriggerDependenciesComposer {
337 middleware::HttpMiddlewareComposer
338 }
339
340 fn supported_host_requirements() -> Vec<&'static str> {
341 vec![
342 spin_app::locked::SERVICE_CHAINING_KEY,
343 spin_app::locked::MIDDLEWARE_KEY,
344 ]
345 }
346
347 fn display_name() -> String {
348 "HTTP".to_string()
349 }
350}
351
352impl HttpTrigger {
353 pub fn new(
355 app: &spin_app::App,
356 listen_addr: SocketAddr,
357 tls_config: Option<TlsConfig>,
358 find_free_port: bool,
359 http1_max_buf_size: Option<usize>,
360 reuse_config: InstanceReuseConfig,
361 output_format: OutputFormat,
362 ) -> anyhow::Result<Self> {
363 Self::validate_app(app)?;
364
365 Ok(Self {
366 listen_addr,
367 tls_config,
368 find_free_port,
369 http1_max_buf_size,
370 reuse_config,
371 output_format,
372 })
373 }
374
375 pub fn into_server<F: RuntimeFactors>(
377 self,
378 trigger_app: TriggerApp<F>,
379 ) -> anyhow::Result<Arc<HttpServer<F>>> {
380 let Self {
381 listen_addr,
382 tls_config,
383 find_free_port,
384 http1_max_buf_size,
385 reuse_config,
386 output_format,
387 } = self;
388 let server = Arc::new(HttpServer::new(
389 listen_addr,
390 tls_config,
391 find_free_port,
392 trigger_app,
393 http1_max_buf_size,
394 reuse_config,
395 output_format,
396 )?);
397 Ok(server)
398 }
399
400 fn validate_app(app: &App) -> anyhow::Result<()> {
401 use spin_http::{
402 config::{HttpExecutorType, HttpTriggerConfig},
403 routes::HttpTriggerRouteConfig,
404 };
405
406 #[derive(Deserialize)]
407 #[serde(deny_unknown_fields)]
408 struct TriggerMetadata {
409 base: Option<String>,
410 }
411 if let Some(TriggerMetadata { base: Some(base) }) = app.get_trigger_metadata("http")? {
412 if base == "/" {
413 tracing::warn!(
414 "This application has the deprecated trigger 'base' set to the default value '/'. This may be an error in the future!"
415 );
416 } else {
417 bail!(
418 "This application is using the deprecated trigger 'base' field. The base must be prepended to each [[trigger.http]]'s 'route'."
419 )
420 }
421 }
422
423 let mut explain_wagi_deprecation = false;
424 for trigger in app.triggers_with_type("http") {
425 if let Ok(config) = trigger.typed_config::<HttpTriggerConfig>()
426 && let Some(executor) = config.executor
427 && let HttpExecutorType::Wagi(_) = executor
428 {
429 let description = match config.route {
430 HttpTriggerRouteConfig::Route(r) => format!("route {r}"),
431 HttpTriggerRouteConfig::Private(_) => format!(
432 "private endpoint for {}",
433 config.component.unwrap_or_else(|| "<unknown>".into())
434 ),
435 };
436 terminal::warn!("HTTP {description} uses the WAGI executor.");
437 explain_wagi_deprecation = true;
438 }
439 }
440 if explain_wagi_deprecation {
441 terminal::warn!("WAGI will be deprecated in a future version of Spin.");
442 eprintln!(
443 "To provide feedback, please visit https://github.com/spinframework/spin/issues/3520.\n"
444 );
445 }
446
447 Ok(())
448 }
449}
450
451fn parse_listen_addr(addr: &str) -> anyhow::Result<SocketAddr> {
452 let addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
453 if let Some(addr) = addrs
455 .iter()
456 .find(|addr| addr.is_ipv4() && addr.ip() == Ipv4Addr::LOCALHOST)
457 {
458 return Ok(*addr);
459 }
460 addrs.into_iter().next().context("couldn't resolve address")
462}
463
464#[derive(Debug, PartialEq)]
465enum NotFoundRouteKind {
466 Normal(String),
467 WellKnown,
468}
469
470pub fn hyper_request_error(err: hyper::Error) -> ErrorCode {
472 if let Some(cause) = err.source()
474 && let Some(err) = cause.downcast_ref::<ErrorCode>()
475 {
476 return err.clone();
477 }
478
479 tracing::warn!("hyper request error: {err:?}");
480
481 ErrorCode::HttpProtocolError
482}
483
484pub fn dns_error(rcode: String, info_code: u16) -> ErrorCode {
485 ErrorCode::DnsError(
486 wasmtime_wasi_http::p2::bindings::http::types::DnsErrorPayload {
487 rcode: Some(rcode),
488 info_code: Some(info_code),
489 },
490 )
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496
497 #[test]
498 fn parse_listen_addr_prefers_ipv4() {
499 let addr = parse_listen_addr("localhost:12345").unwrap();
500 assert_eq!(addr.ip(), Ipv4Addr::LOCALHOST);
501 assert_eq!(addr.port(), 12345);
502 }
503
504 #[test]
505 fn request_deadline_config_is_single_use() {
506 let timeout = Duration::from_millis(500);
507 let config = InstanceReuseConfig::single_use_with_request_deadline(timeout);
508
509 assert!(matches!(config.max_instance_reuse_count, Range::Value(1)));
510 assert!(matches!(
511 config.max_instance_concurrent_reuse_count,
512 Range::Value(1)
513 ));
514 assert!(matches!(config.request_timeout, Some(Range::Value(value)) if value == timeout));
515 assert_eq!(config.request_deadline, Some(timeout));
516 }
517}