1use std::{
2 collections::HashMap,
3 io::{ErrorKind, IsTerminal},
4 marker::PhantomData,
5 net::SocketAddr,
6 pin::Pin,
7 sync::{Arc, OnceLock},
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::handler::{
48 HandlerState, Instance, Proxy, ShouldAccept, ViewFn, WorkerExpiration, WorkerState,
49 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 component_trigger_configs: HashMap<spin_http::routes::TriggerLookupKey, HttpTriggerConfig>,
103 component_handler_types: HashMap<String, HandlerType<HttpHandlerState<F>>>,
105}
106
107impl<F: RuntimeFactors> HttpServer<F> {
108 pub fn new(
110 listen_addr: SocketAddr,
111 tls_config: Option<TlsConfig>,
112 find_free_port: bool,
113 trigger_app: TriggerApp<F>,
114 http1_max_buf_size: Option<usize>,
115 reuse_config: InstanceReuseConfig,
116 output_format: OutputFormat,
117 ) -> anyhow::Result<Self> {
118 let component_trigger_configs = trigger_app
120 .app()
121 .trigger_configs::<HttpTriggerConfig>("http")?
122 .into_iter()
123 .map(|(trigger_id, config)| config.lookup_key(trigger_id).map(|k| (k, config)))
124 .collect::<Result<Vec<_>, _>>()?;
125
126 let component_routes = component_trigger_configs
128 .iter()
129 .map(|(key, config)| (key, &config.route));
130 let mut duplicate_routes = Vec::new();
131 let router = Router::build("/", component_routes, Some(&mut duplicate_routes))?;
132 if !duplicate_routes.is_empty() {
133 tracing::error!(
134 "The following component routes are duplicates and will never be used:"
135 );
136 for dup in &duplicate_routes {
137 tracing::error!(
138 " {}: {} (duplicate of {})",
139 dup.replaced_id,
140 dup.route(),
141 dup.effective_id,
142 );
143 }
144 }
145 if router.contains_reserved_route() {
146 tracing::error!(
147 "Routes under {} are handled by the Spin runtime and will never be reached",
148 spin_http::WELL_KNOWN_PREFIX
149 );
150 }
151 tracing::trace!(
152 "Constructed router: {:?}",
153 router.routes().collect::<Vec<_>>()
154 );
155
156 let component_trigger_configs = HashMap::from_iter(component_trigger_configs);
158
159 let trigger_app = Arc::new(trigger_app);
160
161 let component_handler_types = component_trigger_configs
162 .iter()
163 .filter_map(|(key, trigger_config)| match key {
164 spin_http::routes::TriggerLookupKey::Component(component) => Some(
165 Self::handler_type_for_component(
166 &trigger_app,
167 component,
168 &trigger_config.executor,
169 reuse_config,
170 )
171 .map(|ht| (component.clone(), ht)),
172 ),
173 spin_http::routes::TriggerLookupKey::Trigger(_) => None,
174 })
175 .collect::<anyhow::Result<_>>()?;
176 Ok(Self {
177 listen_addr,
178 local_addr: OnceLock::new(),
179 tls_config,
180 find_free_port,
181 router,
182 trigger_app,
183 http1_max_buf_size,
184 component_trigger_configs,
185 component_handler_types,
186 output_format,
187 request_deadline: reuse_config.request_deadline,
188 })
189 }
190
191 fn handler_type_for_component(
192 trigger_app: &Arc<TriggerApp<F>>,
193 component_id: &str,
194 executor: &Option<HttpExecutorType>,
195 reuse_config: InstanceReuseConfig,
196 ) -> anyhow::Result<HandlerType<HttpHandlerState<F>>> {
197 let pre = trigger_app.get_instance_pre(component_id)?;
198 let handler_type = match executor {
199 None | Some(HttpExecutorType::Http) => HandlerType::from_instance_pre(
200 pre,
201 HttpHandlerState {
202 component_id: component_id.into(),
203 reuse_config,
204 server: Default::default(),
205 self_scheme: Default::default(),
206 },
207 )?,
208 Some(HttpExecutorType::Wagi(wagi_config)) => {
209 anyhow::ensure!(
210 wagi_config.entrypoint == "_start",
211 "Wagi component '{component_id}' cannot use deprecated 'entrypoint' field"
212 );
213 HandlerType::Wagi(
214 CommandIndices::new(pre)
215 .map_err(anyhow::Error::from)
216 .context("failed to find wasi command interface for wagi executor")?,
217 )
218 }
219 };
220 Ok(handler_type)
221 }
222
223 pub async fn serve(self: Arc<Self>) -> anyhow::Result<()> {
225 let listener: TcpListener = if self.find_free_port {
226 self.search_for_free_port().await?
227 } else {
228 TcpListener::bind(self.listen_addr).await.map_err(|err| {
229 if err.kind() == ErrorKind::AddrInUse {
230 anyhow::anyhow!("{} is already in use. To have Spin search for a free port, use the --find-free-port option.", self.listen_addr)
231 } else {
232 anyhow::anyhow!("Unable to listen on {}: {err:?}", self.listen_addr)
233 }
234 })?
235 };
236
237 let _ = self.local_addr.set(listener.local_addr()?);
238
239 if let Some(tls_config) = self.tls_config.clone() {
240 self.serve_https(listener, tls_config).await?;
241 } else {
242 self.serve_http(listener).await?;
243 }
244 Ok(())
245 }
246
247 async fn search_for_free_port(&self) -> anyhow::Result<TcpListener> {
248 let mut found_listener = None;
249 let mut addr = self.listen_addr;
250
251 for _ in 1..=MAX_RETRIES {
252 if addr.port() == u16::MAX {
253 anyhow::bail!(
254 "Couldn't find a free port as we've reached the maximum port number. Consider retrying with a lower base port."
255 );
256 }
257
258 match TcpListener::bind(addr).await {
259 Ok(listener) => {
260 found_listener = Some(listener);
261 break;
262 }
263 Err(err) if err.kind() == ErrorKind::AddrInUse => {
264 addr.set_port(addr.port() + 1);
265 continue;
266 }
267 Err(err) => anyhow::bail!("Unable to listen on {addr}: {err:?}",),
268 }
269 }
270
271 found_listener.ok_or_else(|| anyhow::anyhow!(
272 "Couldn't find a free port in the range {}-{}. Consider retrying with a different base port.",
273 self.listen_addr.port(),
274 self.listen_addr.port() + MAX_RETRIES
275 ))
276 }
277
278 async fn serve_http(self: Arc<Self>, listener: TcpListener) -> anyhow::Result<()> {
279 self.print_startup_msgs("http", &listener)?;
280 loop {
281 let (stream, client_addr) = listener.accept().await?;
282 self.clone()
283 .serve_connection(stream, Scheme::HTTP, client_addr);
284 }
285 }
286
287 async fn serve_https(
288 self: Arc<Self>,
289 listener: TcpListener,
290 tls_config: TlsConfig,
291 ) -> anyhow::Result<()> {
292 self.print_startup_msgs("https", &listener)?;
293 let acceptor = tls_config.server_config()?;
294 loop {
295 let (stream, client_addr) = listener.accept().await?;
296 match acceptor.accept(stream).await {
297 Ok(stream) => self
298 .clone()
299 .serve_connection(stream, Scheme::HTTPS, client_addr),
300 Err(err) => tracing::error!(?err, "Failed to start TLS session"),
301 }
302 }
303 }
304
305 pub async fn handle(
310 self: &Arc<Self>,
311 mut req: Request<Body>,
312 server_scheme: Scheme,
313 client_addr: SocketAddr,
314 ) -> anyhow::Result<Response<Body>> {
315 strip_forbidden_headers(&mut req);
316
317 spin_telemetry::extract_trace_context(&req);
318
319 let path = req.uri().path().to_string();
320
321 tracing::info!("Processing request on path '{path}'");
322
323 if let Some(well_known) = path.strip_prefix(spin_http::WELL_KNOWN_PREFIX) {
325 return match well_known {
326 "health" => Ok(MatchedRoute::with_response_extension(
327 Response::new(body::full(Bytes::from_static(b"OK"))),
328 path,
329 )),
330 "info" => self.app_info(path),
331 _ => Self::not_found(NotFoundRouteKind::WellKnown),
332 };
333 }
334
335 match self.router.route(&path) {
336 Ok(route_match) => {
337 self.handle_trigger_route(req, route_match, server_scheme, client_addr)
338 .await
339 }
340 Err(_) => Self::not_found(NotFoundRouteKind::Normal(path.to_string())),
341 }
342 }
343
344 pub async fn handle_trigger_route(
346 self: &Arc<Self>,
347 mut req: Request<Body>,
348 route_match: RouteMatch<'_, '_>,
349 server_scheme: Scheme,
350 client_addr: SocketAddr,
351 ) -> anyhow::Result<Response<Body>> {
352 set_req_uri(&mut req, server_scheme)?;
353 let app_id = self
354 .trigger_app
355 .app()
356 .get_metadata(APP_NAME_KEY)?
357 .unwrap_or_else(|| "<unnamed>".into());
358
359 let lookup_key = route_match.lookup_key();
360
361 spin_telemetry::metrics::monotonic_counter!(
362 spin.request_count = 1,
363 trigger_type = "http",
364 app_id = app_id,
365 component_id = lookup_key.to_string()
366 );
367
368 let trigger_config = self
369 .component_trigger_configs
370 .get(lookup_key)
371 .with_context(|| format!("unknown routing destination '{lookup_key}'"))?;
372
373 match (&trigger_config.component, &trigger_config.static_response) {
374 (Some(component), None) => {
375 self.respond_wasm_component(
376 req,
377 route_match,
378 client_addr,
379 component,
380 &trigger_config.executor,
381 )
382 .await
383 }
384 (None, Some(static_response)) => Self::respond_static_response(static_response),
385 (None, None) => Err(anyhow::anyhow!(
387 "Triggers must specify either component or static_response - neither is specified for {}",
388 route_match.raw_route()
389 )),
390 (Some(_), Some(_)) => Err(anyhow::anyhow!(
391 "Triggers must specify either component or static_response - both are specified for {}",
392 route_match.raw_route()
393 )),
394 }
395 }
396
397 fn get_local_addr(&self) -> SocketAddr {
398 self.local_addr.get().copied().unwrap_or(self.listen_addr)
399 }
400
401 async fn respond_wasm_component(
402 self: &Arc<Self>,
403 req: Request<Body>,
404 route_match: RouteMatch<'_, '_>,
405 client_addr: SocketAddr,
406 component_id: &str,
407 executor: &Option<HttpExecutorType>,
408 ) -> anyhow::Result<Response<Body>> {
409 let handler_type = self
411 .component_handler_types
412 .get(component_id)
413 .with_context(|| format!("unknown component ID {component_id:?}"))?;
414 let executor = executor.as_ref().unwrap_or(&HttpExecutorType::Http);
415
416 let res = match executor {
417 HttpExecutorType::Http => match handler_type {
418 HandlerType::Spin => {
419 SpinHttpExecutor
420 .execute(self, &route_match, req, client_addr, component_id)
421 .await
422 }
423 HandlerType::Wasi0_3(handler) => {
424 Wasip3HttpExecutor(handler)
425 .execute(self, &route_match, req, client_addr)
426 .await
427 }
428 HandlerType::Wasi0_2(_)
429 | HandlerType::Wasi2023_11_10(_)
430 | HandlerType::Wasi2023_10_18(_)
431 | HandlerType::Wasi2026_03_15(_) => {
432 WasiHttpExecutor { handler_type }
433 .execute(self, &route_match, req, client_addr, component_id)
434 .await
435 }
436 HandlerType::Wagi(_) => unreachable!(),
437 },
438 HttpExecutorType::Wagi(wagi_config) => {
439 let indices = match handler_type {
440 HandlerType::Wagi(indices) => indices,
441 _ => unreachable!(),
442 };
443 let executor = WagiHttpExecutor {
444 wagi_config,
445 indices,
446 };
447 executor
448 .execute(self, &route_match, req, client_addr, component_id)
449 .await
450 }
451 };
452 match res {
453 Ok(res) => Ok(MatchedRoute::with_response_extension(
454 res,
455 route_match.raw_route(),
456 )),
457 Err(err) => {
458 tracing::error!("Error processing request: {err:?}");
459 instrument_error(&err);
460 Self::internal_error(None, route_match.raw_route())
461 }
462 }
463 }
464
465 pub(crate) fn trigger_instance_builder(
466 self: &'_ Arc<Self>,
467 component_id: &str,
468 self_scheme: Option<&Scheme>,
469 ) -> anyhow::Result<TriggerInstanceBuilder<'_, F>> {
470 let mut instance_builder = self.trigger_app.prepare(component_id)?;
471
472 let outbound_http = instance_builder
477 .factor_builder::<OutboundHttpFactor>()
478 .context(
479 "The wasi HTTP trigger was configured without the required wasi outbound http support",
480 )?;
481
482 let self_scheme = self_scheme.cloned().unwrap_or(Scheme::HTTPS);
483 let self_addr = self.get_local_addr();
484 let origin = SelfRequestOrigin::create(self_scheme, &self_addr.to_string())?;
485 outbound_http.set_self_request_origin(origin);
486 outbound_http.set_request_interceptor(OutboundHttpInterceptor::new(self.clone()))?;
487 Ok(instance_builder)
488 }
489
490 fn respond_static_response(
491 sr: &spin_http::config::StaticResponse,
492 ) -> anyhow::Result<Response<Body>> {
493 let mut response = Response::builder();
494
495 response = response.status(sr.status());
496 for (header_name, header_value) in sr.headers() {
497 response = response.header(header_name, header_value);
498 }
499
500 let body = match sr.body() {
501 Some(b) => body::full(b.clone().into()),
502 None => body::empty(),
503 };
504
505 Ok(response.body(body)?)
506 }
507
508 fn app_info(&self, route: String) -> anyhow::Result<Response<Body>> {
510 let info = AppInfo::new(self.trigger_app.app());
511 let body = serde_json::to_vec_pretty(&info)?;
512 Ok(MatchedRoute::with_response_extension(
513 Response::builder()
514 .header("content-type", "application/json")
515 .body(body::full(body.into()))?,
516 route,
517 ))
518 }
519
520 fn internal_error(
522 body: Option<&str>,
523 route: impl Into<String>,
524 ) -> anyhow::Result<Response<Body>> {
525 let body = match body {
526 Some(body) => body::full(Bytes::copy_from_slice(body.as_bytes())),
527 None => body::empty(),
528 };
529
530 Ok(MatchedRoute::with_response_extension(
531 Response::builder()
532 .status(StatusCode::INTERNAL_SERVER_ERROR)
533 .body(body)?,
534 route,
535 ))
536 }
537
538 fn not_found(kind: NotFoundRouteKind) -> anyhow::Result<Response<Body>> {
540 use std::sync::atomic::{AtomicBool, Ordering};
541 static SHOWN_GENERIC_404_WARNING: AtomicBool = AtomicBool::new(false);
542 if let NotFoundRouteKind::Normal(route) = kind
543 && !SHOWN_GENERIC_404_WARNING.fetch_or(true, Ordering::Relaxed)
544 && std::io::stderr().is_terminal()
545 {
546 terminal::warn!(
547 "Request to {route} matched no pattern, and received a generic 404 response. To serve a more informative 404 page, add a catch-all (/...) route."
548 );
549 }
550 Ok(Response::builder()
551 .status(StatusCode::NOT_FOUND)
552 .body(body::empty())?)
553 }
554
555 fn serve_connection<S: AsyncRead + AsyncWrite + Unpin + Send + 'static>(
556 self: Arc<Self>,
557 stream: S,
558 server_scheme: Scheme,
559 client_addr: SocketAddr,
560 ) {
561 task::spawn(async move {
562 let mut server_builder = Builder::new(TokioExecutor::new());
563
564 if let Some(http1_max_buf_size) = self.http1_max_buf_size {
565 server_builder.http1().max_buf_size(http1_max_buf_size);
566 }
567
568 if let Err(err) = server_builder
569 .serve_connection(
570 TokioIo::new(stream),
571 service_fn(move |request| {
572 self.clone().instrumented_service_fn(
573 server_scheme.clone(),
574 client_addr,
575 request,
576 )
577 }),
578 )
579 .await
580 {
581 tracing::warn!("Error serving HTTP connection: {err:?}");
582 }
583 });
584 }
585
586 async fn instrumented_service_fn(
587 self: Arc<Self>,
588 server_scheme: Scheme,
589 client_addr: SocketAddr,
590 request: Request<Incoming>,
591 ) -> anyhow::Result<Response<HyperOutgoingBody>> {
592 let span = http_span!(request, client_addr);
593 let method = request.method().to_string();
594 async {
595 let result = self
596 .handle(
597 request.map(|body: Incoming| {
598 body.map_err(wasmtime_wasi_http::p2::hyper_response_error)
599 .boxed_unsync()
600 }),
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 RequestId = ();
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::RequestId,
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<Arc<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(|| server.clone());
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 (instance, mut store) = self
825 .server
826 .get()
827 .expect("server should have been set")
828 .trigger_instance_builder(&self.component_id, self.self_scheme.get())
829 .to_wasmtime_result()?
830 .instantiate(())
831 .await
832 .to_wasmtime_result()?;
833 set_request_deadline(&mut store, self.reuse_config.request_deadline);
834
835 let mut store = store.into_inner();
836
837 let proxy = Proxy::P3(Service::new(&mut store, &instance).unwrap());
838
839 let request_timeout = self
840 .reuse_config
841 .request_timeout
842 .map(|range| rand::rng().random_range(range))
843 .unwrap_or(Duration::MAX);
844
845 Ok(Instance {
846 store,
847 proxy,
848 view: ViewFn::P3(|data| {
849 spin_factor_outbound_http::OutboundHttpFactor::get_wasi_p3_http_impl(
850 data.factors_instance_state_mut(),
851 )
852 .unwrap()
853 }),
854 expiration: HttpWorkerExpiration {
855 idle_timeout: rand::rng().random_range(self.reuse_config.idle_instance_timeout),
856 request_timeout,
857 sleep: tokio::time::sleep(Duration::MAX),
858 },
859 state: HttpWorkerState {
860 request_timeout,
861 max_instance_reuse_count: rand::rng()
862 .random_range(self.reuse_config.max_instance_reuse_count),
863 max_instance_concurrent_reuse_count: rand::rng()
864 .random_range(self.reuse_config.max_instance_concurrent_reuse_count),
865 _phantom: PhantomData,
866 },
867 })
868 }
869}