1use std::{
2 collections::HashMap,
3 io::{ErrorKind, IsTerminal},
4 marker::PhantomData,
5 net::SocketAddr,
6 pin::Pin,
7 sync::{Arc, OnceLock, Weak},
8 task::{Context, Poll},
9 time::{Duration, Instant},
10};
11
12use anyhow::{Context as _, bail};
13use http::{
14 Request, Response, StatusCode, Uri,
15 uri::{Authority, Scheme},
16};
17use http_body_util::BodyExt;
18use hyper::{
19 body::{Bytes, Incoming},
20 service::service_fn,
21};
22use hyper_util::{
23 rt::{TokioExecutor, TokioIo},
24 server::conn::auto::Builder,
25};
26use pin_project_lite::pin_project;
27use rand::RngExt;
28use spin_app::{APP_DESCRIPTION_KEY, APP_NAME_KEY};
29use spin_factor_outbound_http::{OutboundHttpFactor, SelfRequestOrigin};
30use spin_factors::RuntimeFactors;
31use spin_factors_executor::InstanceState;
32use spin_http::{
33 app_info::AppInfo,
34 body,
35 config::{HttpExecutorType, HttpTriggerConfig},
36 routes::{RouteInfo, RouteMatch, Router},
37 trigger::HandlerType,
38};
39use tokio::{
40 io::{AsyncRead, AsyncWrite},
41 net::TcpListener,
42 task,
43};
44use tracing::Instrument;
45use wasmtime::{Store, StoreContextMut, ToWasmtimeResult, component::GuestTaskId};
46use wasmtime_wasi::p2::bindings::CommandIndices;
47use wasmtime_wasi_http::Error as WasiHttpError;
48use wasmtime_wasi_http::handler::{
49 HandlerState, Instance, Proxy, ShouldAccept, WorkerExpiration, WorkerState, WorkerStatus,
50};
51use wasmtime_wasi_http::p2::body::HyperOutgoingBody;
52use wasmtime_wasi_http::p3::bindings::Service;
53
54use crate::{
55 Body, InstanceReuseConfig, NotFoundRouteKind, OutputFormat, TlsConfig, TriggerApp,
56 TriggerInstanceBuilder,
57 headers::strip_forbidden_headers,
58 instrument::{MatchedRoute, finalize_http_span, http_span, instrument_error},
59 outbound_http::OutboundHttpInterceptor,
60 spin::SpinHttpExecutor,
61 wagi::WagiHttpExecutor,
62 wasi::WasiHttpExecutor,
63 wasip3::Wasip3HttpExecutor,
64};
65
66pub const MAX_RETRIES: u16 = 10;
67
68pub(crate) fn set_request_deadline<T>(
69 store: &mut spin_core::Store<T>,
70 request_deadline: Option<Duration>,
71) {
72 if let Some(timeout) = request_deadline {
73 store.set_deadline(Instant::now() + timeout);
74 }
75}
76
77pub struct HttpServer<F: RuntimeFactors> {
79 listen_addr: SocketAddr,
81 local_addr: OnceLock<SocketAddr>,
87 tls_config: Option<TlsConfig>,
89 http1_max_buf_size: Option<usize>,
91 find_free_port: bool,
93 output_format: OutputFormat,
95 request_deadline: Option<Duration>,
97 router: Router,
99 trigger_app: Arc<TriggerApp<F>>,
101 app_id: Arc<str>,
103 component_trigger_configs: HashMap<spin_http::routes::TriggerLookupKey, HttpTriggerConfig>,
105 component_handler_types: HashMap<String, HandlerType<HttpHandlerState<F>>>,
107}
108
109impl<F: RuntimeFactors> HttpServer<F> {
110 pub fn new(
112 listen_addr: SocketAddr,
113 tls_config: Option<TlsConfig>,
114 find_free_port: bool,
115 trigger_app: TriggerApp<F>,
116 http1_max_buf_size: Option<usize>,
117 reuse_config: InstanceReuseConfig,
118 output_format: OutputFormat,
119 ) -> anyhow::Result<Self> {
120 let component_trigger_configs = trigger_app
122 .app()
123 .trigger_configs::<HttpTriggerConfig>("http")?
124 .into_iter()
125 .map(|(trigger_id, config)| config.lookup_key(trigger_id).map(|k| (k, config)))
126 .collect::<Result<Vec<_>, _>>()?;
127
128 let component_routes = component_trigger_configs
130 .iter()
131 .map(|(key, config)| (key, &config.route));
132 let mut duplicate_routes = Vec::new();
133 let router = Router::build("/", component_routes, Some(&mut duplicate_routes))?;
134 if !duplicate_routes.is_empty() {
135 tracing::error!(
136 "The following component routes are duplicates and will never be used:"
137 );
138 for dup in &duplicate_routes {
139 tracing::error!(
140 " {}: {} (duplicate of {})",
141 dup.replaced_id,
142 dup.route(),
143 dup.effective_id,
144 );
145 }
146 }
147 if router.contains_reserved_route() {
148 tracing::error!(
149 "Routes under {} are handled by the Spin runtime and will never be reached",
150 spin_http::WELL_KNOWN_PREFIX
151 );
152 }
153 tracing::trace!(
154 "Constructed router: {:?}",
155 router.routes().collect::<Vec<_>>()
156 );
157
158 let component_trigger_configs = HashMap::from_iter(component_trigger_configs);
160
161 let trigger_app = Arc::new(trigger_app);
162
163 let app_id: Arc<str> = trigger_app
164 .app()
165 .get_metadata(APP_NAME_KEY)?
166 .unwrap_or_else(|| "<unnamed>".into())
167 .into();
168
169 let component_handler_types = component_trigger_configs
170 .iter()
171 .filter_map(|(key, trigger_config)| match key {
172 spin_http::routes::TriggerLookupKey::Component(component) => Some(
173 Self::handler_type_for_component(
174 &trigger_app,
175 component,
176 &trigger_config.executor,
177 reuse_config,
178 )
179 .map(|ht| (component.clone(), ht)),
180 ),
181 spin_http::routes::TriggerLookupKey::Trigger(_) => None,
182 })
183 .collect::<anyhow::Result<_>>()?;
184 Ok(Self {
185 listen_addr,
186 local_addr: OnceLock::new(),
187 tls_config,
188 find_free_port,
189 router,
190 trigger_app,
191 app_id,
192 http1_max_buf_size,
193 component_trigger_configs,
194 component_handler_types,
195 output_format,
196 request_deadline: reuse_config.request_deadline,
197 })
198 }
199
200 fn handler_type_for_component(
201 trigger_app: &Arc<TriggerApp<F>>,
202 component_id: &str,
203 executor: &Option<HttpExecutorType>,
204 reuse_config: InstanceReuseConfig,
205 ) -> anyhow::Result<HandlerType<HttpHandlerState<F>>> {
206 let pre = trigger_app.get_instance_pre(component_id)?;
207 let handler_type = match executor {
208 None | Some(HttpExecutorType::Http) => HandlerType::from_instance_pre(
209 pre,
210 HttpHandlerState {
211 component_id: component_id.into(),
212 reuse_config,
213 server: Default::default(),
214 self_scheme: Default::default(),
215 },
216 )?,
217 Some(HttpExecutorType::Wagi(wagi_config)) => {
218 anyhow::ensure!(
219 wagi_config.entrypoint == "_start",
220 "Wagi component '{component_id}' cannot use deprecated 'entrypoint' field"
221 );
222 HandlerType::Wagi(
223 CommandIndices::new(pre)
224 .map_err(anyhow::Error::from)
225 .context("failed to find wasi command interface for wagi executor")?,
226 )
227 }
228 };
229 Ok(handler_type)
230 }
231
232 pub async fn serve(self: Arc<Self>) -> anyhow::Result<()> {
234 let listener: TcpListener = if self.find_free_port {
235 self.search_for_free_port().await?
236 } else {
237 TcpListener::bind(self.listen_addr).await.map_err(|err| {
238 if err.kind() == ErrorKind::AddrInUse {
239 anyhow::anyhow!("{} is already in use. To have Spin search for a free port, use the --find-free-port option.", self.listen_addr)
240 } else {
241 anyhow::anyhow!("Unable to listen on {}: {err:?}", self.listen_addr)
242 }
243 })?
244 };
245
246 let _ = self.local_addr.set(listener.local_addr()?);
247
248 if let Some(tls_config) = self.tls_config.clone() {
249 self.serve_https(listener, tls_config).await?;
250 } else {
251 self.serve_http(listener).await?;
252 }
253 Ok(())
254 }
255
256 async fn search_for_free_port(&self) -> anyhow::Result<TcpListener> {
257 let mut found_listener = None;
258 let mut addr = self.listen_addr;
259
260 for _ in 1..=MAX_RETRIES {
261 if addr.port() == u16::MAX {
262 anyhow::bail!(
263 "Couldn't find a free port as we've reached the maximum port number. Consider retrying with a lower base port."
264 );
265 }
266
267 match TcpListener::bind(addr).await {
268 Ok(listener) => {
269 found_listener = Some(listener);
270 break;
271 }
272 Err(err) if err.kind() == ErrorKind::AddrInUse => {
273 addr.set_port(addr.port() + 1);
274 continue;
275 }
276 Err(err) => anyhow::bail!("Unable to listen on {addr}: {err:?}",),
277 }
278 }
279
280 found_listener.ok_or_else(|| anyhow::anyhow!(
281 "Couldn't find a free port in the range {}-{}. Consider retrying with a different base port.",
282 self.listen_addr.port(),
283 self.listen_addr.port() + MAX_RETRIES
284 ))
285 }
286
287 async fn serve_http(self: Arc<Self>, listener: TcpListener) -> anyhow::Result<()> {
288 self.print_startup_msgs("http", &listener)?;
289 loop {
290 let (stream, client_addr) = listener.accept().await?;
291 self.clone()
292 .serve_connection(stream, Scheme::HTTP, client_addr);
293 }
294 }
295
296 async fn serve_https(
297 self: Arc<Self>,
298 listener: TcpListener,
299 tls_config: TlsConfig,
300 ) -> anyhow::Result<()> {
301 self.print_startup_msgs("https", &listener)?;
302 let acceptor = tls_config.server_config()?;
303 loop {
304 let (stream, client_addr) = listener.accept().await?;
305 match acceptor.accept(stream).await {
306 Ok(stream) => self
307 .clone()
308 .serve_connection(stream, Scheme::HTTPS, client_addr),
309 Err(err) => tracing::error!(?err, "Failed to start TLS session"),
310 }
311 }
312 }
313
314 pub async fn handle(
319 self: &Arc<Self>,
320 mut req: Request<Body>,
321 server_scheme: Scheme,
322 client_addr: SocketAddr,
323 ) -> anyhow::Result<Response<Body>> {
324 strip_forbidden_headers(&mut req);
325
326 spin_telemetry::extract_trace_context(&req);
327
328 let path = req.uri().path().to_string();
329
330 tracing::info!("Processing request on path '{path}'");
331
332 if let Some(well_known) = path.strip_prefix(spin_http::WELL_KNOWN_PREFIX) {
334 return match well_known {
335 "health" => Ok(MatchedRoute::with_response_extension(
336 Response::new(body::full(Bytes::from_static(b"OK"))),
337 path,
338 )),
339 "info" => self.app_info(path),
340 _ => Self::not_found(NotFoundRouteKind::WellKnown),
341 };
342 }
343
344 match self.router.route(&path) {
345 Ok(route_match) => {
346 self.handle_trigger_route(req, route_match, server_scheme, client_addr)
347 .await
348 }
349 Err(_) => Self::not_found(NotFoundRouteKind::Normal(path.to_string())),
350 }
351 }
352
353 pub async fn handle_trigger_route(
355 self: &Arc<Self>,
356 mut req: Request<Body>,
357 route_match: RouteMatch<'_, '_>,
358 server_scheme: Scheme,
359 client_addr: SocketAddr,
360 ) -> anyhow::Result<Response<Body>> {
361 set_req_uri(&mut req, server_scheme)?;
362 let lookup_key = route_match.lookup_key();
363
364 spin_telemetry::metrics::counter!(
365 spin.request_count = 1,
366 trigger_type = "http",
367 app_id = self.app_id.clone(),
368 component_id = lookup_key.to_string()
369 );
370
371 let trigger_config = self
372 .component_trigger_configs
373 .get(lookup_key)
374 .with_context(|| format!("unknown routing destination '{lookup_key}'"))?;
375
376 match (&trigger_config.component, &trigger_config.static_response) {
377 (Some(component), None) => {
378 self.respond_wasm_component(
379 req,
380 route_match,
381 client_addr,
382 component,
383 &trigger_config.executor,
384 )
385 .await
386 }
387 (None, Some(static_response)) => Self::respond_static_response(static_response),
388 (None, None) => Err(anyhow::anyhow!(
390 "Triggers must specify either component or static_response - neither is specified for {}",
391 route_match.raw_route()
392 )),
393 (Some(_), Some(_)) => Err(anyhow::anyhow!(
394 "Triggers must specify either component or static_response - both are specified for {}",
395 route_match.raw_route()
396 )),
397 }
398 }
399
400 fn get_local_addr(&self) -> SocketAddr {
401 self.local_addr.get().copied().unwrap_or(self.listen_addr)
402 }
403
404 async fn respond_wasm_component(
405 self: &Arc<Self>,
406 req: Request<Body>,
407 route_match: RouteMatch<'_, '_>,
408 client_addr: SocketAddr,
409 component_id: &str,
410 executor: &Option<HttpExecutorType>,
411 ) -> anyhow::Result<Response<Body>> {
412 let handler_type = self
414 .component_handler_types
415 .get(component_id)
416 .with_context(|| format!("unknown component ID {component_id:?}"))?;
417 let executor = executor.as_ref().unwrap_or(&HttpExecutorType::Http);
418
419 let res = match executor {
420 HttpExecutorType::Http => match handler_type {
421 HandlerType::Spin => {
422 SpinHttpExecutor
423 .execute(self, &route_match, req, client_addr, component_id)
424 .await
425 }
426 HandlerType::Wasi0_3(handler) => {
427 Wasip3HttpExecutor(handler)
428 .execute(self, &route_match, req, client_addr)
429 .await
430 }
431 HandlerType::Wasi0_2(_)
432 | HandlerType::Wasi2023_11_10(_)
433 | HandlerType::Wasi2023_10_18(_)
434 | HandlerType::Wasi2026_03_15(_) => {
435 WasiHttpExecutor { handler_type }
436 .execute(self, &route_match, req, client_addr, component_id)
437 .await
438 }
439 HandlerType::Wagi(_) => unreachable!(),
440 },
441 HttpExecutorType::Wagi(wagi_config) => {
442 let indices = match handler_type {
443 HandlerType::Wagi(indices) => indices,
444 _ => unreachable!(),
445 };
446 let executor = WagiHttpExecutor {
447 wagi_config,
448 indices,
449 };
450 executor
451 .execute(self, &route_match, req, client_addr, component_id)
452 .await
453 }
454 };
455 match res {
456 Ok(res) => Ok(MatchedRoute::with_response_extension(
457 res,
458 route_match.raw_route(),
459 )),
460 Err(err) => {
461 tracing::error!("Error processing request: {err:?}");
462 instrument_error(&err);
463 Self::internal_error(None, route_match.raw_route())
464 }
465 }
466 }
467
468 pub(crate) fn trigger_instance_builder(
469 self: &'_ Arc<Self>,
470 component_id: &str,
471 self_scheme: Option<&Scheme>,
472 ) -> anyhow::Result<TriggerInstanceBuilder<'_, F>> {
473 let mut instance_builder = self.trigger_app.prepare(component_id)?;
474
475 let outbound_http = instance_builder
480 .factor_builder::<OutboundHttpFactor>()
481 .context(
482 "The wasi HTTP trigger was configured without the required wasi outbound http support",
483 )?;
484
485 let self_scheme = self_scheme.cloned().unwrap_or(Scheme::HTTPS);
486 let self_addr = self.get_local_addr();
487 let origin = SelfRequestOrigin::create(self_scheme, &self_addr.to_string())?;
488 outbound_http.set_self_request_origin(origin);
489 outbound_http.set_request_interceptor(OutboundHttpInterceptor::new(self.clone()))?;
490 Ok(instance_builder)
491 }
492
493 fn respond_static_response(
494 sr: &spin_http::config::StaticResponse,
495 ) -> anyhow::Result<Response<Body>> {
496 let mut response = Response::builder();
497
498 response = response.status(sr.status());
499 for (header_name, header_value) in sr.headers() {
500 response = response.header(header_name, header_value);
501 }
502
503 let body = match sr.body() {
504 Some(b) => body::full(b.clone().into()),
505 None => body::empty(),
506 };
507
508 Ok(response.body(body)?)
509 }
510
511 fn app_info(&self, route: String) -> anyhow::Result<Response<Body>> {
513 let info = AppInfo::new(self.trigger_app.app());
514 let body = serde_json::to_vec_pretty(&info)?;
515 Ok(MatchedRoute::with_response_extension(
516 Response::builder()
517 .header("content-type", "application/json")
518 .body(body::full(body.into()))?,
519 route,
520 ))
521 }
522
523 fn internal_error(
525 body: Option<&str>,
526 route: impl Into<String>,
527 ) -> anyhow::Result<Response<Body>> {
528 let body = match body {
529 Some(body) => body::full(Bytes::copy_from_slice(body.as_bytes())),
530 None => body::empty(),
531 };
532
533 Ok(MatchedRoute::with_response_extension(
534 Response::builder()
535 .status(StatusCode::INTERNAL_SERVER_ERROR)
536 .body(body)?,
537 route,
538 ))
539 }
540
541 fn not_found(kind: NotFoundRouteKind) -> anyhow::Result<Response<Body>> {
543 use std::sync::atomic::{AtomicBool, Ordering};
544 static SHOWN_GENERIC_404_WARNING: AtomicBool = AtomicBool::new(false);
545 if let NotFoundRouteKind::Normal(route) = kind
546 && !SHOWN_GENERIC_404_WARNING.fetch_or(true, Ordering::Relaxed)
547 && std::io::stderr().is_terminal()
548 {
549 terminal::warn!(
550 "Request to {route} matched no pattern, and received a generic 404 response. To serve a more informative 404 page, add a catch-all (/...) route."
551 );
552 }
553 Ok(Response::builder()
554 .status(StatusCode::NOT_FOUND)
555 .body(body::empty())?)
556 }
557
558 fn serve_connection<S: AsyncRead + AsyncWrite + Unpin + Send + 'static>(
559 self: Arc<Self>,
560 stream: S,
561 server_scheme: Scheme,
562 client_addr: SocketAddr,
563 ) {
564 task::spawn(async move {
565 let mut server_builder = Builder::new(TokioExecutor::new());
566
567 if let Some(http1_max_buf_size) = self.http1_max_buf_size {
568 server_builder.http1().max_buf_size(http1_max_buf_size);
569 }
570
571 if let Err(err) = server_builder
572 .serve_connection(
573 TokioIo::new(stream),
574 service_fn(move |request| {
575 self.clone().instrumented_service_fn(
576 server_scheme.clone(),
577 client_addr,
578 request,
579 )
580 }),
581 )
582 .await
583 {
584 tracing::warn!("Error serving HTTP connection: {err:?}");
585 }
586 });
587 }
588
589 async fn instrumented_service_fn(
590 self: Arc<Self>,
591 server_scheme: Scheme,
592 client_addr: SocketAddr,
593 request: Request<Incoming>,
594 ) -> anyhow::Result<Response<HyperOutgoingBody>> {
595 let span = http_span!(request, client_addr);
596 let method = request.method().to_string();
597 async {
598 let result = self
599 .handle(
600 request.map(|body: Incoming| body.map_err(WasiHttpError::from).boxed_unsync()),
601 server_scheme,
602 client_addr,
603 )
604 .await;
605 finalize_http_span(result, method)
606 }
607 .instrument(span)
608 .await
609 }
610
611 fn get_description_for_route(
612 &self,
613 key: &spin_http::routes::TriggerLookupKey,
614 ) -> anyhow::Result<Option<String>> {
615 if let spin_http::routes::TriggerLookupKey::Component(component_id) = key {
616 self.trigger_app
617 .app()
618 .get_component(component_id)
619 .and_then(|c| c.get_metadata(APP_DESCRIPTION_KEY).transpose())
620 .transpose()
621 .map_err(Into::into)
622 } else {
623 Ok(None)
624 }
625 }
626
627 fn print_startup_msgs(&self, scheme: &str, listener: &TcpListener) -> anyhow::Result<()> {
628 let local_addr = listener.local_addr()?;
629 let base_url = format!("{scheme}://{local_addr:?}");
630 tracing::info!("Serving {base_url}");
631
632 match self.output_format {
633 OutputFormat::Plain => {
634 terminal::step!("\nServing", "{base_url}");
635 println!("Available Routes:");
636 for (route, key) in self.router.routes() {
637 println!(" {key}: {base_url}{route}");
638 if let Some(description) = self.get_description_for_route(key)? {
639 println!(" {description}");
640 }
641 }
642 }
643 OutputFormat::Json => {
644 #[derive(serde::Serialize)]
645 struct RoutesOutput {
646 base_url: String,
647 routes: Vec<RouteEntry>,
648 }
649
650 #[derive(serde::Serialize)]
651 struct RouteEntry {
652 id: String,
653 route: String,
654 wildcard: bool,
655 #[serde(skip_serializing_if = "Option::is_none")]
656 description: Option<String>,
657 }
658 let mut routes = Vec::new();
659 for (route, key) in self.router.routes() {
660 routes.push(RouteEntry {
661 id: key.to_string(),
662 route: route.path().to_string(),
663 wildcard: route.is_wildcard(),
664 description: self.get_description_for_route(key)?,
665 });
666 }
667
668 let output = RoutesOutput { base_url, routes };
669 println!("{}", serde_json::to_string_pretty(&output)?);
670 }
671 }
672 Ok(())
673 }
674
675 pub(crate) fn request_deadline(&self) -> Option<Duration> {
676 self.request_deadline
677 }
678}
679
680fn set_req_uri(req: &mut Request<Body>, scheme: Scheme) -> anyhow::Result<()> {
686 let uri = req.uri().clone();
687 let mut parts = uri.into_parts();
688 let headers = req.headers();
689 let header_authority = headers
690 .get(http::header::HOST)
691 .map(|h| -> anyhow::Result<Authority> {
692 let host_header = h.to_str().context("'Host' header is not valid UTF-8")?;
693 host_header
694 .parse()
695 .context("'Host' header contains an invalid authority")
696 })
697 .transpose()?;
698 let uri_authority = parts.authority;
699
700 let authority = match (header_authority, uri_authority) {
702 (None, None) => bail!("no 'Host' header present in request"),
703 (None, Some(a)) => a,
704 (Some(a), None) => a,
705 (Some(a1), Some(a2)) => {
706 if a1 != a2 {
709 return Err(anyhow::anyhow!(
710 "authority in 'Host' header does not match authority in URI"
711 ));
712 }
713 a1
714 }
715 };
716 parts.scheme = Some(scheme);
717 parts.authority = Some(authority);
718 *req.uri_mut() = Uri::from_parts(parts).unwrap();
719 Ok(())
720}
721
722pin_project! {
723 pub(crate) struct HttpWorkerExpiration {
724 idle_timeout: Duration,
725 request_timeout: Duration,
726 #[pin]
727 sleep: tokio::time::Sleep,
728 }
729}
730
731impl WorkerExpiration for HttpWorkerExpiration {
732 fn poll(
733 self: Pin<&mut Self>,
734 cx: &mut Context<'_>,
735 status: WorkerStatus,
736 start: Instant,
737 ) -> Poll<()> {
738 let mut me = self.project();
739
740 let timeout = match status {
741 WorkerStatus::Idle => *me.idle_timeout,
742 WorkerStatus::Requests | WorkerStatus::PostReturn => *me.request_timeout,
746 };
747
748 if let Some(deadline) = start.checked_add(timeout) {
749 let deadline = deadline.into();
750 if deadline != me.sleep.deadline() {
751 me.sleep.as_mut().reset(deadline);
752 }
753 me.sleep.poll(cx)
754 } else {
755 Poll::Pending
756 }
757 }
758}
759
760pub(crate) struct HttpWorkerState<F: RuntimeFactors> {
761 request_timeout: Duration,
762 max_instance_reuse_count: usize,
763 max_instance_concurrent_reuse_count: usize,
764 _phantom: PhantomData<F>,
765}
766
767impl<F: RuntimeFactors> WorkerState for HttpWorkerState<F> {
768 type StoreData = InstanceState<F::InstanceState, ()>;
769 type RequestData = ();
770
771 fn should_accept_request(&self, concurrent_count: usize, total_count: usize) -> ShouldAccept {
772 if total_count >= self.max_instance_reuse_count {
773 ShouldAccept::Never
774 } else if concurrent_count >= self.max_instance_concurrent_reuse_count {
775 ShouldAccept::No
776 } else {
777 ShouldAccept::Yes
778 }
779 }
780
781 fn on_request_start(
782 &self,
783 _: StoreContextMut<'_, Self::StoreData>,
784 _: Self::RequestData,
785 _: GuestTaskId,
786 ) -> Pin<Box<dyn Future<Output = ()> + Send + Sync + 'static>> {
787 Box::pin(tokio::time::sleep(self.request_timeout))
788 }
789
790 fn drop(&self, store: Store<Self::StoreData>, result: Result<(), wasmtime::Error>) {
791 if let Err(error) = result {
792 eprintln!("worker failed: {error:?}");
793 }
794
795 drop(store);
796 }
797}
798
799pub(crate) struct HttpHandlerState<F: RuntimeFactors> {
800 component_id: String,
801 reuse_config: InstanceReuseConfig,
802 server: OnceLock<Weak<HttpServer<F>>>,
803 self_scheme: OnceLock<Scheme>,
804}
805
806impl<F: RuntimeFactors> HttpHandlerState<F> {
807 pub(crate) fn init_once(&self, server: &Arc<HttpServer<F>>, first_uri: &Uri) {
808 self.server.get_or_init(|| Arc::downgrade(server));
809 if let Some(scheme) = first_uri.scheme() {
810 self.self_scheme.get_or_init(|| scheme.clone());
811 }
812 }
813}
814
815impl<F: RuntimeFactors> HandlerState for HttpHandlerState<F> {
816 type StoreData = InstanceState<F::InstanceState, ()>;
817 type WorkerExpiration = HttpWorkerExpiration;
818 type WorkerState = HttpWorkerState<F>;
819
820 async fn instantiate(
821 &self,
822 ) -> wasmtime::Result<Instance<Self::StoreData, Self::WorkerExpiration, Self::WorkerState>>
823 {
824 let server = self
825 .server
826 .get()
827 .expect("server should have been set")
828 .upgrade()
829 .ok_or_else(|| wasmtime::format_err!("HTTP server is no longer available"))?;
830 let (instance, mut store) = server
831 .trigger_instance_builder(&self.component_id, self.self_scheme.get())
832 .to_wasmtime_result()?
833 .instantiate(())
834 .await
835 .to_wasmtime_result()?;
836 set_request_deadline(&mut store, self.reuse_config.request_deadline);
837
838 let mut store = store.into_inner();
839
840 let proxy = Proxy::P3(Service::new(&mut store, &instance).unwrap());
841
842 let request_timeout = self
843 .reuse_config
844 .request_timeout
845 .map(|range| rand::rng().random_range(range))
846 .unwrap_or(Duration::MAX);
847
848 Ok(Instance {
849 store,
850 proxy,
851 view: |data| {
852 spin_factor_outbound_http::OutboundHttpFactor::get_wasi_http_impl(
853 data.factors_instance_state_mut(),
854 )
855 .unwrap()
856 },
857 expiration: HttpWorkerExpiration {
858 idle_timeout: rand::rng().random_range(self.reuse_config.idle_instance_timeout),
859 request_timeout,
860 sleep: tokio::time::sleep(Duration::MAX),
861 },
862 state: HttpWorkerState {
863 request_timeout,
864 max_instance_reuse_count: rand::rng()
865 .random_range(self.reuse_config.max_instance_reuse_count),
866 max_instance_concurrent_reuse_count: rand::rng()
867 .random_range(self.reuse_config.max_instance_concurrent_reuse_count),
868 _phantom: PhantomData,
869 },
870 })
871 }
872}