#![allow(clippy::arc_with_non_send_sync)]
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use bytes::Bytes;
use http_body_util::{combinators::BoxBody, BodyExt, Full};
use sha2::{Digest, Sha256};
use thiserror::Error;
use tokio::sync::RwLock;
use tracing::{debug, info, instrument, warn};
use wasmtime::component::{Component, Linker, ResourceTable};
use wasmtime::{Config, Engine, Store, StoreLimits, StoreLimitsBuilder};
use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
use wasmtime_wasi_http::bindings::http::types::Scheme;
use wasmtime_wasi_http::bindings::{Proxy, ProxyPre};
use wasmtime_wasi_http::body::HyperOutgoingBody;
use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView};
#[allow(deprecated)]
use zlayer_spec::{WasmCapabilities, WasmHttpConfig};
#[derive(Debug, Clone)]
pub struct HttpRequest {
pub method: String,
pub uri: String,
pub headers: Vec<(String, String)>,
pub body: Option<Vec<u8>>,
}
impl HttpRequest {
pub fn get(uri: impl Into<String>) -> Self {
Self {
method: "GET".to_string(),
uri: uri.into(),
headers: Vec::new(),
body: None,
}
}
pub fn post(uri: impl Into<String>, body: Vec<u8>) -> Self {
Self {
method: "POST".to_string(),
uri: uri.into(),
headers: Vec::new(),
body: Some(body),
}
}
pub fn put(uri: impl Into<String>, body: Vec<u8>) -> Self {
Self {
method: "PUT".to_string(),
uri: uri.into(),
headers: Vec::new(),
body: Some(body),
}
}
pub fn delete(uri: impl Into<String>) -> Self {
Self {
method: "DELETE".to_string(),
uri: uri.into(),
headers: Vec::new(),
body: None,
}
}
pub fn patch(uri: impl Into<String>, body: Vec<u8>) -> Self {
Self {
method: "PATCH".to_string(),
uri: uri.into(),
headers: Vec::new(),
body: Some(body),
}
}
#[must_use]
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
}
#[derive(Debug, Clone)]
pub struct HttpResponse {
pub status: u16,
pub headers: Vec<(String, String)>,
pub body: Option<Vec<u8>>,
}
impl HttpResponse {
#[must_use]
pub fn new(status: u16) -> Self {
Self {
status,
headers: Vec::new(),
body: None,
}
}
#[must_use]
pub fn ok() -> Self {
Self::new(200)
}
#[must_use]
pub fn created() -> Self {
Self::new(201)
}
#[must_use]
pub fn no_content() -> Self {
Self::new(204)
}
pub fn bad_request(message: impl Into<String>) -> Self {
let body = message.into().into_bytes();
Self {
status: 400,
headers: vec![("Content-Type".to_string(), "text/plain".to_string())],
body: Some(body),
}
}
#[must_use]
pub fn unauthorized() -> Self {
Self::new(401)
}
#[must_use]
pub fn forbidden() -> Self {
Self::new(403)
}
#[must_use]
pub fn not_found() -> Self {
Self::new(404)
}
pub fn internal_error(message: impl Into<String>) -> Self {
let body = message.into().into_bytes();
Self {
status: 500,
headers: vec![("Content-Type".to_string(), "text/plain".to_string())],
body: Some(body),
}
}
#[must_use]
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
#[must_use]
pub fn with_body(mut self, body: Vec<u8>) -> Self {
self.body = Some(body);
self
}
}
#[derive(Error, Debug)]
pub enum WasmHttpError {
#[error("failed to create wasmtime engine: {0}")]
EngineCreation(String),
#[error("failed to compile component '{component}': {reason}")]
Compilation { component: String, reason: String },
#[error("failed to instantiate component '{component}': {reason}")]
Instantiation { component: String, reason: String },
#[error("failed to invoke handler for '{component}': {reason}")]
HandlerInvocation { component: String, reason: String },
#[error("request timed out after {timeout:?}")]
Timeout { timeout: Duration },
#[error("component '{component}' not found in cache")]
ComponentNotFound { component: String },
#[error("instance pool exhausted for component '{component}'")]
PoolExhausted { component: String },
#[error("invalid WASM component: {reason}")]
InvalidComponent { reason: String },
#[error("internal error: {0}")]
Internal(String),
}
impl From<wasmtime::Error> for WasmHttpError {
fn from(err: wasmtime::Error) -> Self {
WasmHttpError::Internal(err.to_string())
}
}
pub struct WasmHttpState {
wasi_ctx: WasiCtx,
http_ctx: WasiHttpCtx,
table: ResourceTable,
limiter: StoreLimits,
}
impl WasmHttpState {
fn new() -> Self {
let wasi_ctx = WasiCtxBuilder::new().inherit_stdio().inherit_env().build();
Self {
wasi_ctx,
http_ctx: WasiHttpCtx::new(),
table: ResourceTable::new(),
limiter: StoreLimitsBuilder::new().build(),
}
}
fn with_capabilities(capabilities: &WasmCapabilities) -> Self {
let mut builder = WasiCtxBuilder::new();
super::wasm_host::configure_wasi_ctx_with_capabilities(&mut builder, capabilities);
let wasi_ctx = builder.build();
Self {
wasi_ctx,
http_ctx: WasiHttpCtx::new(),
table: ResourceTable::new(),
limiter: StoreLimitsBuilder::new().build(),
}
}
#[allow(dead_code)] fn with_limits(store_limits: StoreLimits) -> Self {
let wasi_ctx = WasiCtxBuilder::new().inherit_stdio().inherit_env().build();
Self {
wasi_ctx,
http_ctx: WasiHttpCtx::new(),
table: ResourceTable::new(),
limiter: store_limits,
}
}
#[allow(dead_code)] fn with_capabilities_and_limits(
capabilities: &WasmCapabilities,
store_limits: StoreLimits,
) -> Self {
let mut builder = WasiCtxBuilder::new();
super::wasm_host::configure_wasi_ctx_with_capabilities(&mut builder, capabilities);
let wasi_ctx = builder.build();
Self {
wasi_ctx,
http_ctx: WasiHttpCtx::new(),
table: ResourceTable::new(),
limiter: store_limits,
}
}
}
impl WasiView for WasmHttpState {
fn ctx(&mut self) -> WasiCtxView<'_> {
WasiCtxView {
ctx: &mut self.wasi_ctx,
table: &mut self.table,
}
}
}
impl WasiHttpView for WasmHttpState {
fn ctx(&mut self) -> &mut WasiHttpCtx {
&mut self.http_ctx
}
fn table(&mut self) -> &mut ResourceTable {
&mut self.table
}
}
struct RequestInstance {
store: Store<WasmHttpState>,
proxy: Proxy,
#[allow(dead_code)]
created_at: Instant,
}
impl std::fmt::Debug for RequestInstance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RequestInstance")
.field("created_at", &self.created_at)
.finish_non_exhaustive()
}
}
#[derive(Debug)]
pub struct InstancePool {
#[allow(dead_code)]
max_idle: usize,
#[allow(dead_code)]
max_age: Duration,
total_created: AtomicU64,
total_destroyed: AtomicU64,
total_requests: AtomicU64,
}
impl InstancePool {
fn new(max_idle: usize, max_age: Duration) -> Self {
Self {
max_idle,
max_age,
total_created: AtomicU64::new(0),
total_destroyed: AtomicU64::new(0),
total_requests: AtomicU64::new(0),
}
}
#[allow(dead_code)]
pub fn max_idle(&self) -> usize {
self.max_idle
}
#[allow(dead_code)]
pub fn max_age(&self) -> Duration {
self.max_age
}
fn record_created(&self) {
self.total_created.fetch_add(1, Ordering::Relaxed);
}
fn record_destroyed(&self) {
self.total_destroyed.fetch_add(1, Ordering::Relaxed);
}
fn record_request(&self) {
self.total_requests.fetch_add(1, Ordering::Relaxed);
}
fn total_created(&self) -> u64 {
self.total_created.load(Ordering::Relaxed)
}
fn total_destroyed(&self) -> u64 {
self.total_destroyed.load(Ordering::Relaxed)
}
fn total_requests(&self) -> u64 {
self.total_requests.load(Ordering::Relaxed)
}
}
struct CompiledComponent {
component: Component,
compiled_at: Instant,
}
impl std::fmt::Debug for CompiledComponent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CompiledComponent")
.field("compiled_at", &self.compiled_at)
.field("age", &self.compiled_at.elapsed())
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Default)]
pub struct PoolStats {
pub cached_components: usize,
pub total_idle_instances: usize,
pub total_created: u64,
pub total_destroyed: u64,
pub total_requests: u64,
pub components: HashMap<String, ComponentStats>,
}
#[derive(Debug, Clone, Default)]
pub struct ComponentStats {
pub idle_instances: usize,
pub total_created: u64,
pub total_destroyed: u64,
pub total_requests: u64,
}
#[allow(deprecated)]
pub struct WasmHttpRuntime {
engine: Engine,
module_cache: Arc<RwLock<HashMap<String, CompiledComponent>>>,
instance_pools: Arc<RwLock<HashMap<String, InstancePool>>>,
config: WasmHttpConfig,
linker: Arc<Linker<WasmHttpState>>,
cache_dir: PathBuf,
capabilities: Option<WasmCapabilities>,
store_limits: Option<StoreLimits>,
max_fuel: u64,
}
impl std::fmt::Debug for WasmHttpRuntime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WasmHttpRuntime")
.field("config", &self.config)
.field("capabilities", &self.capabilities)
.finish_non_exhaustive()
}
}
impl WasmHttpRuntime {
#[allow(deprecated)]
pub fn new(config: WasmHttpConfig) -> Result<Self, WasmHttpError> {
let mut engine_config = Config::new();
engine_config.async_support(true);
engine_config.wasm_component_model(true);
engine_config.epoch_interruption(true);
engine_config.consume_fuel(true);
let engine = Engine::new(&engine_config)
.map_err(|e| WasmHttpError::EngineCreation(e.to_string()))?;
let mut linker = Linker::new(&engine);
wasmtime_wasi::p2::add_to_linker_async(&mut linker)
.map_err(|e| WasmHttpError::EngineCreation(format!("failed to add WASI: {e}")))?;
wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)
.map_err(|e| WasmHttpError::EngineCreation(format!("failed to add WASI HTTP: {e}")))?;
let cache_dir = zlayer_paths::ZLayerDirs::system_default().wasm_compiled();
if let Err(e) = std::fs::create_dir_all(&cache_dir) {
warn!(
cache_dir = %cache_dir.display(),
error = %e,
"Failed to create WASM AOT cache directory (disk caching will be attempted on first compile)"
);
}
info!(cache_dir = %cache_dir.display(), "WASM HTTP runtime initialized (all capabilities)");
Ok(Self {
engine,
module_cache: Arc::new(RwLock::new(HashMap::new())),
instance_pools: Arc::new(RwLock::new(HashMap::new())),
config,
linker: Arc::new(linker),
cache_dir,
capabilities: None,
store_limits: None,
max_fuel: 0,
})
}
#[allow(deprecated)]
pub fn new_with_capabilities(
config: WasmHttpConfig,
capabilities: WasmCapabilities,
) -> Result<Self, WasmHttpError> {
let mut engine_config = Config::new();
engine_config.async_support(true);
engine_config.wasm_component_model(true);
engine_config.epoch_interruption(true);
engine_config.consume_fuel(true);
let engine = Engine::new(&engine_config)
.map_err(|e| WasmHttpError::EngineCreation(e.to_string()))?;
let mut linker = Linker::new(&engine);
wasmtime_wasi::p2::add_to_linker_async(&mut linker)
.map_err(|e| WasmHttpError::EngineCreation(format!("failed to add WASI: {e}")))?;
if capabilities.http_client {
wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker).map_err(|e| {
WasmHttpError::EngineCreation(format!("failed to add WASI HTTP: {e}"))
})?;
debug!("WASI HTTP outgoing handler linked");
} else {
debug!("WASI HTTP outgoing handler NOT linked (capability disabled)");
}
let cache_dir = zlayer_paths::ZLayerDirs::system_default().wasm_compiled();
if let Err(e) = std::fs::create_dir_all(&cache_dir) {
warn!(
cache_dir = %cache_dir.display(),
error = %e,
"Failed to create WASM AOT cache directory (disk caching will be attempted on first compile)"
);
}
info!(
cache_dir = %cache_dir.display(),
?capabilities,
"WASM HTTP runtime initialized with capabilities"
);
Ok(Self {
engine,
module_cache: Arc::new(RwLock::new(HashMap::new())),
instance_pools: Arc::new(RwLock::new(HashMap::new())),
config,
linker: Arc::new(linker),
cache_dir,
capabilities: Some(capabilities),
store_limits: None,
max_fuel: 0,
})
}
#[must_use]
pub fn capabilities(&self) -> Option<&WasmCapabilities> {
self.capabilities.as_ref()
}
pub fn set_resource_limits(&mut self, spec_wasm: &zlayer_spec::WasmConfig) {
if let Some(ref max_memory) = spec_wasm.max_memory {
match super::wasm::parse_memory_limit(max_memory) {
Ok(max_bytes) => {
#[allow(clippy::cast_possible_truncation)]
let mem_size = max_bytes as usize;
self.store_limits =
Some(StoreLimitsBuilder::new().memory_size(mem_size).build());
info!(
max_bytes = max_bytes,
"WASM HTTP store memory limit configured"
);
}
Err(e) => {
warn!(max_memory = %max_memory, error = %e, "ignoring invalid max_memory");
}
}
}
self.max_fuel = spec_wasm.max_fuel;
if self.max_fuel > 0 {
info!(max_fuel = self.max_fuel, "WASM HTTP fuel budget configured");
}
}
#[allow(deprecated)]
#[instrument(skip(self, wasm_bytes, request), fields(component = %component_ref, method = %request.method, uri = %request.uri))]
pub async fn handle_request(
&self,
component_ref: &str,
wasm_bytes: &[u8],
request: HttpRequest,
) -> Result<HttpResponse, WasmHttpError> {
let timeout = self.config.request_timeout;
let component = self.get_or_compile(component_ref, wasm_bytes).await?;
let mut instance = self.create_instance(component_ref, &component).await?;
self.record_request(component_ref).await;
let component_ref_owned = component_ref.to_string();
let result = tokio::time::timeout(timeout, async {
self.invoke_handler(&mut instance, &component_ref_owned, request)
.await
})
.await;
self.record_destroyed(component_ref).await;
match result {
Ok(Ok(response)) => Ok(response),
Ok(Err(e)) => Err(e),
Err(_) => Err(WasmHttpError::Timeout { timeout }),
}
}
#[allow(clippy::used_underscore_binding, deprecated)]
#[instrument(skip(self, wasm_bytes), fields(component = %component_ref))]
pub async fn prewarm(
&self,
component_ref: &str,
wasm_bytes: &[u8],
_count: usize,
) -> Result<(), WasmHttpError> {
info!("Pre-warming component cache");
let _ = self.get_or_compile(component_ref, wasm_bytes).await?;
{
let mut pools = self.instance_pools.write().await;
pools.entry(component_ref.to_string()).or_insert_with(|| {
InstancePool::new(self.config.max_instances as usize, self.config.idle_timeout)
});
}
Ok(())
}
pub async fn pool_stats(&self) -> PoolStats {
let pools = self.instance_pools.read().await;
let cache = self.module_cache.read().await;
let mut stats = PoolStats {
cached_components: cache.len(),
..Default::default()
};
for (name, pool) in pools.iter() {
stats.total_created += pool.total_created();
stats.total_destroyed += pool.total_destroyed();
stats.total_requests += pool.total_requests();
stats.components.insert(
name.clone(),
ComponentStats {
idle_instances: 0, total_created: pool.total_created(),
total_destroyed: pool.total_destroyed(),
total_requests: pool.total_requests(),
},
);
}
stats
}
pub async fn clear_cache(&self) {
let mut cache = self.module_cache.write().await;
cache.clear();
if self.cache_dir.exists() {
if let Err(e) = tokio::fs::remove_dir_all(&self.cache_dir).await {
warn!(
cache_dir = %self.cache_dir.display(),
error = %e,
"Failed to remove WASM AOT disk cache directory"
);
} else if let Err(e) = tokio::fs::create_dir_all(&self.cache_dir).await {
warn!(
cache_dir = %self.cache_dir.display(),
error = %e,
"Failed to recreate WASM AOT disk cache directory"
);
}
}
info!("Component cache cleared (in-memory and disk)");
}
fn content_hash(wasm_bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(wasm_bytes);
format!("{:x}", hasher.finalize())
}
#[allow(clippy::too_many_lines, unsafe_code)]
async fn get_or_compile(
&self,
component_ref: &str,
wasm_bytes: &[u8],
) -> Result<Arc<Component>, WasmHttpError> {
{
let cache = self.module_cache.read().await;
if let Some(compiled) = cache.get(component_ref) {
debug!(
component = component_ref,
"Using in-memory cached component"
);
return Ok(Arc::new(compiled.component.clone()));
}
}
let hash = Self::content_hash(wasm_bytes);
let cache_path = self.cache_dir.join(format!("{hash}.cwasm"));
if cache_path.exists() {
match tokio::fs::read(&cache_path).await {
Ok(serialized) => {
let engine = self.engine.clone();
let cache_path_display = cache_path.display().to_string();
let component_ref_owned = component_ref.to_string();
match tokio::task::spawn_blocking(move || {
unsafe { Component::deserialize(&engine, &serialized) }
})
.await
{
Ok(Ok(component)) => {
info!(
component = component_ref,
cache_path = cache_path_display,
"Loaded pre-compiled WASM component from disk cache"
);
let mut cache = self.module_cache.write().await;
cache.insert(
component_ref.to_string(),
CompiledComponent {
component: component.clone(),
compiled_at: Instant::now(),
},
);
return Ok(Arc::new(component));
}
Ok(Err(e)) => {
warn!(
component = component_ref_owned,
error = %e,
"Failed to deserialize cached component, recompiling"
);
let _ = tokio::fs::remove_file(&cache_path).await;
}
Err(e) => {
warn!(
component = component_ref_owned,
error = %e,
"Disk cache deserialization task failed, recompiling"
);
let _ = tokio::fs::remove_file(&cache_path).await;
}
}
}
Err(e) => {
debug!(
component = component_ref,
error = %e,
"Disk cache file not readable, compiling from scratch"
);
}
}
}
info!(component = component_ref, "Compiling WASM component");
let engine = self.engine.clone();
let bytes = wasm_bytes.to_vec();
let component_ref_owned = component_ref.to_string();
let component = tokio::task::spawn_blocking(move || {
Component::new(&engine, &bytes).map_err(|e| WasmHttpError::Compilation {
component: component_ref_owned,
reason: e.to_string(),
})
})
.await
.map_err(|e| WasmHttpError::Internal(format!("task join error: {e}")))??;
match component.serialize() {
Ok(serialized) => {
let cache_dir = self.cache_dir.clone();
let cache_path_clone = cache_path.clone();
let component_ref_clone = component_ref.to_string();
tokio::spawn(async move {
if let Err(e) = tokio::fs::create_dir_all(&cache_dir).await {
warn!(error = %e, "Failed to create WASM AOT cache directory");
return;
}
if let Err(e) = tokio::fs::write(&cache_path_clone, &serialized).await {
warn!(
component = component_ref_clone,
error = %e,
"Failed to write WASM component to disk cache"
);
} else {
debug!(
component = component_ref_clone,
cache_path = %cache_path_clone.display(),
size_bytes = serialized.len(),
"Cached pre-compiled WASM component to disk"
);
}
});
}
Err(e) => {
warn!(
component = component_ref,
error = %e,
"Failed to serialize WASM component for disk caching"
);
}
}
{
let mut cache = self.module_cache.write().await;
cache.insert(
component_ref.to_string(),
CompiledComponent {
component: component.clone(),
compiled_at: Instant::now(),
},
);
}
Ok(Arc::new(component))
}
#[allow(deprecated)]
async fn create_instance(
&self,
component_ref: &str,
component: &Component,
) -> Result<RequestInstance, WasmHttpError> {
let linker = Arc::clone(&self.linker);
let component_ref_owned = component_ref.to_string();
{
let mut pools = self.instance_pools.write().await;
let pool = pools.entry(component_ref.to_string()).or_insert_with(|| {
InstancePool::new(self.config.max_instances as usize, self.config.idle_timeout)
});
pool.record_created();
}
let proxy_pre = {
let component_ref_owned = component_ref_owned.clone();
let instance_pre =
linker
.instantiate_pre(component)
.map_err(|e| WasmHttpError::Instantiation {
component: component_ref_owned.clone(),
reason: format!("failed to create instance pre: {e}"),
})?;
ProxyPre::new(instance_pre).map_err(|e| WasmHttpError::Instantiation {
component: component_ref_owned,
reason: format!("failed to create proxy pre: {e}"),
})?
};
let state = match (&self.capabilities, &self.store_limits) {
(Some(caps), Some(limits)) => {
let mut s = WasmHttpState::with_capabilities(caps);
s.limiter = limits.clone();
s
}
(Some(caps), None) => WasmHttpState::with_capabilities(caps),
(None, Some(limits)) => WasmHttpState::with_limits(limits.clone()),
(None, None) => WasmHttpState::new(),
};
let mut store = Store::new(&self.engine, state);
store.limiter(|s| &mut s.limiter);
store.set_epoch_deadline(1_000_000);
if self.max_fuel > 0 {
store
.set_fuel(self.max_fuel)
.map_err(|e| WasmHttpError::Internal(format!("failed to set fuel: {e}")))?;
}
let proxy = proxy_pre.instantiate_async(&mut store).await.map_err(|e| {
WasmHttpError::Instantiation {
component: component_ref.to_string(),
reason: format!("failed to instantiate proxy: {e}"),
}
})?;
debug!("Created new instance for {}", component_ref);
Ok(RequestInstance {
store,
proxy,
created_at: Instant::now(),
})
}
async fn record_request(&self, component_ref: &str) {
let pools = self.instance_pools.read().await;
if let Some(pool) = pools.get(component_ref) {
pool.record_request();
}
}
async fn record_destroyed(&self, component_ref: &str) {
let pools = self.instance_pools.read().await;
if let Some(pool) = pools.get(component_ref) {
pool.record_destroyed();
}
}
async fn invoke_handler(
&self,
instance: &mut RequestInstance,
component_ref: &str,
request: HttpRequest,
) -> Result<HttpResponse, WasmHttpError> {
debug!(
"Handling request: {} {} with {} headers",
request.method,
request.uri,
request.headers.len()
);
instance.store.epoch_deadline_trap();
let hyper_request = self.convert_to_hyper_request(&request).map_err(|e| {
WasmHttpError::HandlerInvocation {
component: component_ref.to_string(),
reason: format!("failed to convert request: {e}"),
}
})?;
let (response_sender, response_receiver) = tokio::sync::oneshot::channel();
let incoming_request = instance
.store
.data_mut()
.new_incoming_request(Scheme::Http, hyper_request)
.map_err(|e| WasmHttpError::HandlerInvocation {
component: component_ref.to_string(),
reason: format!("failed to create incoming request resource: {e}"),
})?;
let response_outparam = instance
.store
.data_mut()
.new_response_outparam(response_sender)
.map_err(|e| WasmHttpError::HandlerInvocation {
component: component_ref.to_string(),
reason: format!("failed to create response outparam resource: {e}"),
})?;
let handler = instance.proxy.wasi_http_incoming_handler();
let component_ref_owned = component_ref.to_string();
let call_result = handler
.call_handle(&mut instance.store, incoming_request, response_outparam)
.await;
if let Err(e) = &call_result {
warn!(
"Handler call failed for component {}: {}",
component_ref_owned, e
);
}
let hyper_response = match response_receiver.await {
Ok(Ok(resp)) => resp,
Ok(Err(error_code)) => {
return Err(WasmHttpError::HandlerInvocation {
component: component_ref.to_string(),
reason: format!("guest returned error: {error_code:?}"),
});
}
Err(_) => {
return Err(WasmHttpError::HandlerInvocation {
component: component_ref.to_string(),
reason: "guest never invoked response-outparam.set".to_string(),
});
}
};
let response = self
.convert_from_hyper_response(hyper_response)
.await
.map_err(|e| WasmHttpError::HandlerInvocation {
component: component_ref.to_string(),
reason: format!("failed to convert response: {e}"),
})?;
debug!(
"Handler completed successfully: status={}, body_len={}",
response.status,
response.body.as_ref().map_or(0, std::vec::Vec::len)
);
Ok(response)
}
#[allow(clippy::unused_self)]
fn convert_to_hyper_request(
&self,
request: &HttpRequest,
) -> Result<hyper::Request<BoxBody<Bytes, hyper::Error>>, anyhow::Error> {
use http::Method;
let method = match request.method.to_uppercase().as_str() {
"GET" => Method::GET,
"POST" => Method::POST,
"PUT" => Method::PUT,
"DELETE" => Method::DELETE,
"HEAD" => Method::HEAD,
"OPTIONS" => Method::OPTIONS,
"PATCH" => Method::PATCH,
"TRACE" => Method::TRACE,
"CONNECT" => Method::CONNECT,
other => Method::from_bytes(other.as_bytes())?,
};
let mut builder = hyper::Request::builder().method(method).uri(&request.uri);
for (name, value) in &request.headers {
builder = builder.header(name.as_str(), value.as_str());
}
let has_host = request
.headers
.iter()
.any(|(name, _)| name.eq_ignore_ascii_case("host"));
if !has_host {
if let Ok(uri) = request.uri.parse::<http::Uri>() {
if let Some(authority) = uri.authority() {
builder = builder.header("Host", authority.as_str());
} else {
builder = builder.header("Host", "localhost");
}
} else {
builder = builder.header("Host", "localhost");
}
}
let body = match &request.body {
Some(bytes) => Full::new(Bytes::from(bytes.clone())),
None => Full::new(Bytes::new()),
};
let boxed_body = body
.map_err(|e: std::convert::Infallible| match e {})
.boxed();
Ok(builder.body(boxed_body)?)
}
async fn convert_from_hyper_response(
&self,
response: hyper::Response<HyperOutgoingBody>,
) -> Result<HttpResponse, anyhow::Error> {
let (parts, body) = response.into_parts();
let status = parts.status.as_u16();
let headers: Vec<(String, String)> = parts
.headers
.iter()
.filter_map(|(name, value)| {
value
.to_str()
.ok()
.map(|v| (name.to_string(), v.to_string()))
})
.collect();
let body_bytes = body
.collect()
.await
.map_err(|e| anyhow::anyhow!("failed to collect response body: {e}"))?
.to_bytes();
let body = if body_bytes.is_empty() {
None
} else {
Some(body_bytes.to_vec())
};
Ok(HttpResponse {
status,
headers,
body,
})
}
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use super::*;
use std::collections::HashMap;
fn test_config() -> WasmHttpConfig {
WasmHttpConfig {
min_instances: 0,
max_instances: 10,
idle_timeout: Duration::from_secs(60),
request_timeout: Duration::from_secs(30),
}
}
fn custom_config(min: u32, max: u32, idle_secs: u64, request_secs: u64) -> WasmHttpConfig {
WasmHttpConfig {
min_instances: min,
max_instances: max,
idle_timeout: Duration::from_secs(idle_secs),
request_timeout: Duration::from_secs(request_secs),
}
}
#[test]
fn test_http_request_creation_with_all_fields() {
let request = HttpRequest {
method: "PUT".to_string(),
uri: "/api/users/123".to_string(),
headers: vec![
("Content-Type".to_string(), "application/json".to_string()),
("Authorization".to_string(), "Bearer token123".to_string()),
],
body: Some(b"{\"name\": \"test\"}".to_vec()),
};
assert_eq!(request.method, "PUT");
assert_eq!(request.uri, "/api/users/123");
assert_eq!(request.headers.len(), 2);
assert_eq!(
request.headers[0],
("Content-Type".to_string(), "application/json".to_string())
);
assert_eq!(
request.headers[1],
("Authorization".to_string(), "Bearer token123".to_string())
);
assert_eq!(request.body, Some(b"{\"name\": \"test\"}".to_vec()));
}
#[test]
fn test_http_request_get_helper() {
let request = HttpRequest::get("/api/test");
assert_eq!(request.method, "GET");
assert_eq!(request.uri, "/api/test");
assert!(request.headers.is_empty());
assert!(request.body.is_none());
}
#[test]
fn test_http_request_get_with_string_type() {
let uri = String::from("/api/resource");
let request = HttpRequest::get(uri);
assert_eq!(request.method, "GET");
assert_eq!(request.uri, "/api/resource");
}
#[test]
fn test_http_request_post_helper() {
let body = b"test body content".to_vec();
let request = HttpRequest::post("/api/submit", body.clone());
assert_eq!(request.method, "POST");
assert_eq!(request.uri, "/api/submit");
assert!(request.headers.is_empty());
assert_eq!(request.body, Some(body));
}
#[test]
fn test_http_request_post_with_empty_body() {
let request = HttpRequest::post("/api/submit", Vec::new());
assert_eq!(request.method, "POST");
assert_eq!(request.body, Some(Vec::new()));
}
#[test]
fn test_http_request_with_header_builder() {
let request = HttpRequest::get("/api/test")
.with_header("Content-Type", "application/json")
.with_header("Authorization", "Bearer token")
.with_header("X-Custom-Header", "custom-value");
assert_eq!(request.headers.len(), 3);
assert_eq!(
request.headers[0],
("Content-Type".to_string(), "application/json".to_string())
);
assert_eq!(
request.headers[1],
("Authorization".to_string(), "Bearer token".to_string())
);
assert_eq!(
request.headers[2],
("X-Custom-Header".to_string(), "custom-value".to_string())
);
}
#[test]
fn test_http_request_with_header_string_types() {
let header_name = String::from("X-Request-Id");
let header_value = String::from("abc-123");
let request = HttpRequest::get("/test").with_header(header_name, header_value);
assert_eq!(request.headers.len(), 1);
assert_eq!(
request.headers[0],
("X-Request-Id".to_string(), "abc-123".to_string())
);
}
#[test]
fn test_http_request_debug_formatting() {
let request = HttpRequest::get("/api/test").with_header("Content-Type", "text/plain");
let debug_str = format!("{request:?}");
assert!(debug_str.contains("HttpRequest"));
assert!(debug_str.contains("GET"));
assert!(debug_str.contains("/api/test"));
assert!(debug_str.contains("Content-Type"));
}
#[test]
fn test_http_request_clone() {
let original =
HttpRequest::post("/api/data", b"body".to_vec()).with_header("X-Test", "value");
let cloned = original.clone();
assert_eq!(cloned.method, original.method);
assert_eq!(cloned.uri, original.uri);
assert_eq!(cloned.headers, original.headers);
assert_eq!(cloned.body, original.body);
}
#[test]
fn test_http_response_new() {
let response = HttpResponse::new(201);
assert_eq!(response.status, 201);
assert!(response.headers.is_empty());
assert!(response.body.is_none());
}
#[test]
fn test_http_response_ok_helper() {
let response = HttpResponse::ok();
assert_eq!(response.status, 200);
assert!(response.headers.is_empty());
assert!(response.body.is_none());
}
#[test]
fn test_http_response_internal_error_helper() {
let response = HttpResponse::internal_error("Something went wrong");
assert_eq!(response.status, 500);
assert_eq!(response.headers.len(), 1);
assert_eq!(
response.headers[0],
("Content-Type".to_string(), "text/plain".to_string())
);
assert_eq!(
response.body,
Some("Something went wrong".as_bytes().to_vec())
);
}
#[test]
fn test_http_response_internal_error_with_string_type() {
let error_msg = String::from("Database connection failed");
let response = HttpResponse::internal_error(error_msg);
assert_eq!(response.status, 500);
assert_eq!(
response.body,
Some("Database connection failed".as_bytes().to_vec())
);
}
#[test]
fn test_http_response_internal_error_empty_message() {
let response = HttpResponse::internal_error("");
assert_eq!(response.status, 500);
assert_eq!(response.body, Some(Vec::new()));
}
#[test]
fn test_http_response_with_header_builder() {
let response = HttpResponse::ok()
.with_header("Content-Type", "application/json")
.with_header("X-Request-Id", "abc-123")
.with_header("Cache-Control", "no-cache");
assert_eq!(response.headers.len(), 3);
assert_eq!(
response.headers[0],
("Content-Type".to_string(), "application/json".to_string())
);
assert_eq!(
response.headers[1],
("X-Request-Id".to_string(), "abc-123".to_string())
);
assert_eq!(
response.headers[2],
("Cache-Control".to_string(), "no-cache".to_string())
);
}
#[test]
fn test_http_response_with_body_builder() {
let body = b"{\"status\": \"ok\"}".to_vec();
let response = HttpResponse::ok().with_body(body.clone());
assert_eq!(response.body, Some(body));
}
#[test]
fn test_http_response_with_body_empty() {
let response = HttpResponse::ok().with_body(Vec::new());
assert_eq!(response.body, Some(Vec::new()));
}
#[test]
fn test_http_response_builder_chain() {
let response = HttpResponse::new(201)
.with_header("Content-Type", "application/json")
.with_header("Location", "/api/users/456")
.with_body(b"{\"id\": 456}".to_vec());
assert_eq!(response.status, 201);
assert_eq!(response.headers.len(), 2);
assert_eq!(response.body, Some(b"{\"id\": 456}".to_vec()));
}
#[test]
fn test_http_response_debug_formatting() {
let response = HttpResponse::ok()
.with_header("Content-Type", "text/html")
.with_body(b"<html>".to_vec());
let debug_str = format!("{response:?}");
assert!(debug_str.contains("HttpResponse"));
assert!(debug_str.contains("200"));
assert!(debug_str.contains("Content-Type"));
}
#[test]
fn test_http_response_clone() {
let original = HttpResponse::ok()
.with_header("X-Test", "value")
.with_body(b"test".to_vec());
let cloned = original.clone();
assert_eq!(cloned.status, original.status);
assert_eq!(cloned.headers, original.headers);
assert_eq!(cloned.body, original.body);
}
#[test]
fn test_http_response_various_status_codes() {
assert_eq!(HttpResponse::new(100).status, 100); assert_eq!(HttpResponse::new(204).status, 204); assert_eq!(HttpResponse::new(301).status, 301); assert_eq!(HttpResponse::new(400).status, 400); assert_eq!(HttpResponse::new(401).status, 401); assert_eq!(HttpResponse::new(403).status, 403); assert_eq!(HttpResponse::new(404).status, 404); assert_eq!(HttpResponse::new(500).status, 500); assert_eq!(HttpResponse::new(502).status, 502); assert_eq!(HttpResponse::new(503).status, 503); }
#[test]
fn test_wasm_http_error_engine_creation_display() {
let error = WasmHttpError::EngineCreation("invalid config".to_string());
let msg = error.to_string();
assert!(msg.contains("failed to create wasmtime engine"));
assert!(msg.contains("invalid config"));
}
#[test]
fn test_wasm_http_error_compilation_display() {
let error = WasmHttpError::Compilation {
component: "my-handler".to_string(),
reason: "invalid wasm bytes".to_string(),
};
let msg = error.to_string();
assert!(msg.contains("failed to compile component"));
assert!(msg.contains("my-handler"));
assert!(msg.contains("invalid wasm bytes"));
}
#[test]
fn test_wasm_http_error_instantiation_display() {
let error = WasmHttpError::Instantiation {
component: "api-service".to_string(),
reason: "missing import".to_string(),
};
let msg = error.to_string();
assert!(msg.contains("failed to instantiate component"));
assert!(msg.contains("api-service"));
assert!(msg.contains("missing import"));
}
#[test]
fn test_wasm_http_error_handler_invocation_display() {
let error = WasmHttpError::HandlerInvocation {
component: "request-handler".to_string(),
reason: "panic in wasm".to_string(),
};
let msg = error.to_string();
assert!(msg.contains("failed to invoke handler"));
assert!(msg.contains("request-handler"));
assert!(msg.contains("panic in wasm"));
}
#[test]
fn test_wasm_http_error_timeout_display() {
let error = WasmHttpError::Timeout {
timeout: Duration::from_secs(30),
};
let msg = error.to_string();
assert!(msg.contains("request timed out"));
assert!(msg.contains("30"));
}
#[test]
fn test_wasm_http_error_timeout_display_millis() {
let error = WasmHttpError::Timeout {
timeout: Duration::from_millis(500),
};
let msg = error.to_string();
assert!(msg.contains("timed out"));
assert!(msg.contains("500"));
}
#[test]
fn test_wasm_http_error_component_not_found_display() {
let error = WasmHttpError::ComponentNotFound {
component: "missing-component".to_string(),
};
let msg = error.to_string();
assert!(msg.contains("not found in cache"));
assert!(msg.contains("missing-component"));
}
#[test]
fn test_wasm_http_error_pool_exhausted_display() {
let error = WasmHttpError::PoolExhausted {
component: "busy-service".to_string(),
};
let msg = error.to_string();
assert!(msg.contains("instance pool exhausted"));
assert!(msg.contains("busy-service"));
}
#[test]
fn test_wasm_http_error_invalid_component_display() {
let error = WasmHttpError::InvalidComponent {
reason: "not a component".to_string(),
};
let msg = error.to_string();
assert!(msg.contains("invalid WASM component"));
assert!(msg.contains("not a component"));
}
#[test]
fn test_wasm_http_error_internal_display() {
let error = WasmHttpError::Internal("unexpected state".to_string());
let msg = error.to_string();
assert!(msg.contains("internal error"));
assert!(msg.contains("unexpected state"));
}
#[test]
fn test_wasm_http_error_debug_formatting() {
let error = WasmHttpError::Compilation {
component: "test".to_string(),
reason: "error".to_string(),
};
let debug_str = format!("{error:?}");
assert!(debug_str.contains("Compilation"));
assert!(debug_str.contains("test"));
assert!(debug_str.contains("error"));
}
#[test]
fn test_wasm_http_error_all_variants_are_errors() {
let errors: Vec<WasmHttpError> = vec![
WasmHttpError::EngineCreation("test".to_string()),
WasmHttpError::Compilation {
component: "c".to_string(),
reason: "r".to_string(),
},
WasmHttpError::Instantiation {
component: "c".to_string(),
reason: "r".to_string(),
},
WasmHttpError::HandlerInvocation {
component: "c".to_string(),
reason: "r".to_string(),
},
WasmHttpError::Timeout {
timeout: Duration::from_secs(1),
},
WasmHttpError::ComponentNotFound {
component: "c".to_string(),
},
WasmHttpError::PoolExhausted {
component: "c".to_string(),
},
WasmHttpError::InvalidComponent {
reason: "r".to_string(),
},
WasmHttpError::Internal("i".to_string()),
];
for error in errors {
let msg = error.to_string();
assert!(!msg.is_empty(), "Error Display should not be empty");
}
}
#[test]
fn test_instance_pool_new_with_correct_settings() {
let pool = InstancePool::new(10, Duration::from_secs(120));
assert_eq!(pool.max_idle(), 10);
assert_eq!(pool.max_age(), Duration::from_secs(120));
assert_eq!(pool.total_created(), 0);
assert_eq!(pool.total_destroyed(), 0);
assert_eq!(pool.total_requests(), 0);
}
#[test]
fn test_instance_pool_new_with_zero_max_idle() {
let pool = InstancePool::new(0, Duration::from_secs(60));
assert_eq!(pool.max_idle(), 0);
}
#[test]
fn test_instance_pool_new_with_zero_max_age() {
let pool = InstancePool::new(5, Duration::ZERO);
assert_eq!(pool.max_age(), Duration::ZERO);
}
#[test]
fn test_instance_pool_atomic_counter_total_created() {
let pool = InstancePool::new(10, Duration::from_secs(60));
assert_eq!(pool.total_created(), 0);
pool.record_created();
assert_eq!(pool.total_created(), 1);
pool.record_created();
assert_eq!(pool.total_created(), 2);
pool.record_created();
pool.record_created();
pool.record_created();
assert_eq!(pool.total_created(), 5);
}
#[test]
fn test_instance_pool_atomic_counter_total_destroyed() {
let pool = InstancePool::new(10, Duration::from_secs(60));
assert_eq!(pool.total_destroyed(), 0);
pool.record_destroyed();
assert_eq!(pool.total_destroyed(), 1);
pool.record_destroyed();
pool.record_destroyed();
assert_eq!(pool.total_destroyed(), 3);
}
#[test]
fn test_instance_pool_atomic_counter_total_requests() {
let pool = InstancePool::new(10, Duration::from_secs(60));
assert_eq!(pool.total_requests(), 0);
pool.record_request();
assert_eq!(pool.total_requests(), 1);
pool.record_request();
pool.record_request();
pool.record_request();
assert_eq!(pool.total_requests(), 4);
}
#[test]
fn test_instance_pool_statistics_combined() {
let pool = InstancePool::new(10, Duration::from_secs(60));
pool.record_created();
pool.record_request();
pool.record_destroyed();
pool.record_created();
pool.record_request();
pool.record_request();
pool.record_request();
pool.record_destroyed();
assert_eq!(pool.total_created(), 2);
assert_eq!(pool.total_destroyed(), 2);
assert_eq!(pool.total_requests(), 4);
}
#[test]
fn test_instance_pool_debug_formatting() {
let pool = InstancePool::new(5, Duration::from_secs(30));
pool.record_created();
pool.record_request();
let debug_str = format!("{pool:?}");
assert!(debug_str.contains("InstancePool"));
}
#[test]
fn test_pool_stats_creation() {
let stats = PoolStats {
cached_components: 3,
total_idle_instances: 5,
total_created: 100,
total_destroyed: 95,
total_requests: 1000,
components: HashMap::new(),
};
assert_eq!(stats.cached_components, 3);
assert_eq!(stats.total_idle_instances, 5);
assert_eq!(stats.total_created, 100);
assert_eq!(stats.total_destroyed, 95);
assert_eq!(stats.total_requests, 1000);
}
#[test]
fn test_pool_stats_default() {
let stats = PoolStats::default();
assert_eq!(stats.cached_components, 0);
assert_eq!(stats.total_idle_instances, 0);
assert_eq!(stats.total_created, 0);
assert_eq!(stats.total_destroyed, 0);
assert_eq!(stats.total_requests, 0);
assert!(stats.components.is_empty());
}
#[test]
fn test_pool_stats_debug_formatting() {
let stats = PoolStats {
cached_components: 2,
total_requests: 50,
..Default::default()
};
let debug_str = format!("{stats:?}");
assert!(debug_str.contains("PoolStats"));
assert!(debug_str.contains("cached_components"));
assert!(debug_str.contains('2'));
}
#[test]
fn test_pool_stats_clone() {
let mut original = PoolStats {
cached_components: 5,
total_requests: 100,
..Default::default()
};
original.components.insert(
"test-component".to_string(),
ComponentStats {
idle_instances: 2,
total_created: 10,
total_destroyed: 8,
total_requests: 50,
},
);
let cloned = original.clone();
assert_eq!(cloned.cached_components, original.cached_components);
assert_eq!(cloned.total_requests, original.total_requests);
assert_eq!(cloned.components.len(), original.components.len());
assert_eq!(
cloned
.components
.get("test-component")
.unwrap()
.idle_instances,
2
);
}
#[test]
fn test_pool_stats_with_multiple_components() {
let mut stats = PoolStats::default();
stats.components.insert(
"api-handler".to_string(),
ComponentStats {
idle_instances: 3,
total_created: 50,
total_destroyed: 47,
total_requests: 500,
},
);
stats.components.insert(
"auth-handler".to_string(),
ComponentStats {
idle_instances: 1,
total_created: 20,
total_destroyed: 19,
total_requests: 200,
},
);
assert_eq!(stats.components.len(), 2);
assert!(stats.components.contains_key("api-handler"));
assert!(stats.components.contains_key("auth-handler"));
}
#[test]
fn test_component_stats_creation() {
let stats = ComponentStats {
idle_instances: 5,
total_created: 100,
total_destroyed: 95,
total_requests: 1000,
};
assert_eq!(stats.idle_instances, 5);
assert_eq!(stats.total_created, 100);
assert_eq!(stats.total_destroyed, 95);
assert_eq!(stats.total_requests, 1000);
}
#[test]
fn test_component_stats_default() {
let stats = ComponentStats::default();
assert_eq!(stats.idle_instances, 0);
assert_eq!(stats.total_created, 0);
assert_eq!(stats.total_destroyed, 0);
assert_eq!(stats.total_requests, 0);
}
#[test]
fn test_component_stats_debug_formatting() {
let stats = ComponentStats {
idle_instances: 2,
total_created: 10,
total_destroyed: 8,
total_requests: 50,
};
let debug_str = format!("{stats:?}");
assert!(debug_str.contains("ComponentStats"));
assert!(debug_str.contains("idle_instances"));
assert!(debug_str.contains("total_created"));
}
#[test]
fn test_component_stats_clone() {
let original = ComponentStats {
idle_instances: 3,
total_created: 25,
total_destroyed: 22,
total_requests: 150,
};
let cloned = original.clone();
assert_eq!(cloned.idle_instances, original.idle_instances);
assert_eq!(cloned.total_created, original.total_created);
assert_eq!(cloned.total_destroyed, original.total_destroyed);
assert_eq!(cloned.total_requests, original.total_requests);
}
#[tokio::test]
async fn test_runtime_new_with_default_config() {
let config = WasmHttpConfig::default();
let result = WasmHttpRuntime::new(config);
assert!(result.is_ok(), "Failed to create runtime: {result:?}");
}
#[tokio::test]
async fn test_runtime_new_with_custom_config() {
let config = custom_config(2, 20, 120, 60);
let result = WasmHttpRuntime::new(config.clone());
assert!(
result.is_ok(),
"Failed to create runtime with custom config"
);
let runtime = result.unwrap();
assert_eq!(runtime.config.min_instances, 2);
assert_eq!(runtime.config.max_instances, 20);
assert_eq!(runtime.config.idle_timeout, Duration::from_secs(120));
assert_eq!(runtime.config.request_timeout, Duration::from_secs(60));
}
#[tokio::test]
async fn test_runtime_new_with_zero_instances() {
let config = custom_config(0, 0, 0, 1);
let result = WasmHttpRuntime::new(config);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_runtime_new_with_large_values() {
let config = custom_config(100, 10000, 3600, 300);
let result = WasmHttpRuntime::new(config);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_runtime_pool_stats_returns_correct_initial_statistics() {
let config = test_config();
let runtime = WasmHttpRuntime::new(config).unwrap();
let stats = runtime.pool_stats().await;
assert_eq!(stats.cached_components, 0);
assert_eq!(stats.total_idle_instances, 0);
assert_eq!(stats.total_created, 0);
assert_eq!(stats.total_destroyed, 0);
assert_eq!(stats.total_requests, 0);
assert!(stats.components.is_empty());
}
#[tokio::test]
async fn test_runtime_clear_cache_empties_component_cache() {
let config = test_config();
let runtime = WasmHttpRuntime::new(config).unwrap();
runtime.clear_cache().await;
let stats = runtime.pool_stats().await;
assert_eq!(stats.cached_components, 0);
}
#[tokio::test]
async fn test_runtime_clear_cache_multiple_times() {
let config = test_config();
let runtime = WasmHttpRuntime::new(config).unwrap();
runtime.clear_cache().await;
runtime.clear_cache().await;
runtime.clear_cache().await;
let stats = runtime.pool_stats().await;
assert_eq!(stats.cached_components, 0);
}
#[tokio::test]
async fn test_runtime_debug_formatting() {
let config = test_config();
let runtime = WasmHttpRuntime::new(config).unwrap();
let debug_str = format!("{runtime:?}");
assert!(debug_str.contains("WasmHttpRuntime"));
assert!(debug_str.contains("config"));
}
#[test]
fn test_wasm_http_config_defaults() {
let config = WasmHttpConfig::default();
assert_eq!(config.min_instances, 0);
assert_eq!(config.max_instances, 10);
assert_eq!(config.idle_timeout, Duration::from_secs(300));
assert_eq!(config.request_timeout, Duration::from_secs(30));
}
#[test]
fn test_wasm_http_config_custom_min_instances() {
let config = WasmHttpConfig {
min_instances: 5,
..Default::default()
};
assert_eq!(config.min_instances, 5);
assert_eq!(config.max_instances, 10); }
#[test]
fn test_wasm_http_config_custom_max_instances() {
let config = WasmHttpConfig {
max_instances: 100,
..Default::default()
};
assert_eq!(config.max_instances, 100);
assert_eq!(config.min_instances, 0); }
#[test]
fn test_wasm_http_config_custom_idle_timeout() {
let config = WasmHttpConfig {
idle_timeout: Duration::from_secs(600),
..Default::default()
};
assert_eq!(config.idle_timeout, Duration::from_secs(600));
}
#[test]
fn test_wasm_http_config_custom_request_timeout() {
let config = WasmHttpConfig {
request_timeout: Duration::from_secs(60),
..Default::default()
};
assert_eq!(config.request_timeout, Duration::from_secs(60));
}
#[test]
fn test_wasm_http_config_fully_custom() {
let config = WasmHttpConfig {
min_instances: 2,
max_instances: 50,
idle_timeout: Duration::from_secs(180),
request_timeout: Duration::from_secs(45),
};
assert_eq!(config.min_instances, 2);
assert_eq!(config.max_instances, 50);
assert_eq!(config.idle_timeout, Duration::from_secs(180));
assert_eq!(config.request_timeout, Duration::from_secs(45));
}
#[test]
fn test_wasm_http_config_clone() {
let original = WasmHttpConfig {
min_instances: 3,
max_instances: 30,
idle_timeout: Duration::from_secs(120),
request_timeout: Duration::from_secs(15),
};
let cloned = original.clone();
assert_eq!(cloned.min_instances, original.min_instances);
assert_eq!(cloned.max_instances, original.max_instances);
assert_eq!(cloned.idle_timeout, original.idle_timeout);
assert_eq!(cloned.request_timeout, original.request_timeout);
}
#[test]
fn test_wasm_http_config_debug_formatting() {
let config = WasmHttpConfig::default();
let debug_str = format!("{config:?}");
assert!(debug_str.contains("WasmHttpConfig"));
assert!(debug_str.contains("min_instances"));
assert!(debug_str.contains("max_instances"));
}
#[test]
fn test_wasm_http_config_equality() {
let config1 = WasmHttpConfig {
min_instances: 1,
max_instances: 10,
idle_timeout: Duration::from_secs(60),
request_timeout: Duration::from_secs(30),
};
let config2 = WasmHttpConfig {
min_instances: 1,
max_instances: 10,
idle_timeout: Duration::from_secs(60),
request_timeout: Duration::from_secs(30),
};
let config3 = WasmHttpConfig {
min_instances: 2, max_instances: 10,
idle_timeout: Duration::from_secs(60),
request_timeout: Duration::from_secs(30),
};
assert_eq!(config1, config2);
assert_ne!(config1, config3);
}
#[test]
fn test_wasmtime_error_conversion() {
let internal_error = WasmHttpError::Internal("simulated wasmtime error".to_string());
let msg = internal_error.to_string();
assert!(msg.contains("internal error"));
}
#[tokio::test]
async fn test_instance_pool_concurrent_record_operations() {
use std::sync::Arc;
let pool = Arc::new(InstancePool::new(100, Duration::from_secs(60)));
let mut handles = vec![];
for _ in 0..10 {
let pool_clone = Arc::clone(&pool);
handles.push(tokio::spawn(async move {
for _ in 0..100 {
pool_clone.record_created();
pool_clone.record_request();
pool_clone.record_destroyed();
}
}));
}
for handle in handles {
handle.await.unwrap();
}
assert_eq!(pool.total_created(), 1000);
assert_eq!(pool.total_requests(), 1000);
assert_eq!(pool.total_destroyed(), 1000);
}
#[tokio::test]
async fn test_runtime_pool_stats_concurrent_access() {
use std::sync::Arc;
let config = test_config();
let runtime = Arc::new(WasmHttpRuntime::new(config).unwrap());
let mut handles = vec![];
for _ in 0..10 {
let runtime_clone = Arc::clone(&runtime);
handles.push(tokio::spawn(async move {
for _ in 0..10 {
let _stats = runtime_clone.pool_stats().await;
}
}));
}
for handle in handles {
handle.await.unwrap();
}
let stats = runtime.pool_stats().await;
assert_eq!(stats.cached_components, 0);
}
#[tokio::test]
async fn test_runtime_clear_cache_concurrent() {
use std::sync::Arc;
let config = test_config();
let runtime = Arc::new(WasmHttpRuntime::new(config).unwrap());
let mut handles = vec![];
for _ in 0..10 {
let runtime_clone = Arc::clone(&runtime);
handles.push(tokio::spawn(async move {
for _ in 0..10 {
runtime_clone.clear_cache().await;
}
}));
}
for handle in handles {
handle.await.unwrap();
}
let stats = runtime.pool_stats().await;
assert_eq!(stats.cached_components, 0);
}
#[test]
fn test_http_request_empty_uri() {
let request = HttpRequest::get("");
assert_eq!(request.uri, "");
}
#[test]
fn test_http_request_unicode_uri() {
let request = HttpRequest::get("/api/users/\u{1F600}");
assert!(request.uri.contains('\u{1F600}'));
}
#[test]
fn test_http_request_very_long_uri() {
let long_uri = "/".to_string() + &"a".repeat(10000);
let request = HttpRequest::get(&long_uri);
assert_eq!(request.uri.len(), 10001);
}
#[test]
fn test_http_response_very_large_body() {
let large_body = vec![0u8; 1_000_000]; let response = HttpResponse::ok().with_body(large_body.clone());
assert_eq!(response.body.as_ref().unwrap().len(), 1_000_000);
}
#[test]
fn test_http_request_many_headers() {
let mut request = HttpRequest::get("/test");
for i in 0..1000 {
request = request.with_header(format!("X-Header-{i}"), format!("value-{i}"));
}
assert_eq!(request.headers.len(), 1000);
}
#[test]
fn test_http_response_many_headers() {
let mut response = HttpResponse::ok();
for i in 0..1000 {
response = response.with_header(format!("X-Header-{i}"), format!("value-{i}"));
}
assert_eq!(response.headers.len(), 1000);
}
#[test]
fn test_wasm_http_state_implements_wasi_http_view() {
fn assert_wasi_http_view<T: WasiHttpView>() {}
assert_wasi_http_view::<WasmHttpState>();
}
#[tokio::test]
async fn test_wasm_http_outgoing_configured() {
let config = WasmHttpConfig::default();
let runtime = WasmHttpRuntime::new(config);
assert!(
runtime.is_ok(),
"Failed to create runtime with HTTP outgoing support: {:?}",
runtime.err()
);
let runtime = runtime.unwrap();
let stats = runtime.pool_stats().await;
assert_eq!(
stats.cached_components, 0,
"Fresh runtime should have no cached components"
);
}
#[test]
fn test_wasm_http_state_send_request_available() {
use wasmtime_wasi_http::types::OutgoingRequestConfig;
let mut state = WasmHttpState::new();
let _ctx = WasiHttpView::ctx(&mut state);
let _table = WasiHttpView::table(&mut state);
let _config = OutgoingRequestConfig {
use_tls: false,
connect_timeout: std::time::Duration::from_secs(30),
first_byte_timeout: std::time::Duration::from_secs(30),
between_bytes_timeout: std::time::Duration::from_secs(30),
};
}
}