diff --git a/lib/wasi-web/Cargo.lock b/lib/wasi-web/Cargo.lock index 703efd4524c..8e7d9cc088f 100644 --- a/lib/wasi-web/Cargo.lock +++ b/lib/wasi-web/Cargo.lock @@ -2504,6 +2504,7 @@ dependencies = [ "dummy-waker", "fastrand", "futures", + "http", "js-sys", "once_cell", "parking_lot", diff --git a/lib/wasi-web/Cargo.toml b/lib/wasi-web/Cargo.toml index ff7f0676928..95d7700b414 100644 --- a/lib/wasi-web/Cargo.toml +++ b/lib/wasi-web/Cargo.toml @@ -41,6 +41,7 @@ dummy-waker = "^1" wat = "1.0" anyhow = "1.0.66" futures = "0.3.25" +http = "0.2.9" [dependencies.parking_lot] version = "^0.11" diff --git a/lib/wasi-web/src/common.rs b/lib/wasi-web/src/common.rs index e64bf87d3a1..2f13eb84d2f 100644 --- a/lib/wasi-web/src/common.rs +++ b/lib/wasi-web/src/common.rs @@ -141,7 +141,7 @@ pub async fn fetch( method: &str, _gzip: bool, cors_proxy: Option, - headers: Vec<(String, String)>, + headers: &http::HeaderMap, data: Option>, ) -> Result { let mut opts = RequestInit::new(); @@ -162,7 +162,8 @@ pub async fn fetch( let set_headers = request.headers(); for (name, val) in headers.iter() { - set_headers.set(name.as_str(), val.as_str()).map_err(|_| { + let val = String::from_utf8_lossy(val.as_bytes()); + set_headers.set(name.as_str(), &val).map_err(|_| { anyhow::anyhow!("could not apply request header: '{name}': '{val}'") })?; } diff --git a/lib/wasi-web/src/glue.rs b/lib/wasi-web/src/glue.rs index 52ce3b30a63..e41026cdede 100644 --- a/lib/wasi-web/src/glue.rs +++ b/lib/wasi-web/src/glue.rs @@ -9,7 +9,6 @@ use tokio::sync::mpsc; use tracing::{debug, error, info, trace, warn}; use wasm_bindgen::{prelude::*, JsCast}; use wasmer_wasix::{ - bin_factory::ModuleCache, capabilities::Capabilities, os::{Console, InputEvent, Tty, TtyOptions}, Pipe, @@ -38,9 +37,9 @@ pub fn main() { set_panic_hook(); } -pub const DEFAULT_BOOT_WEBC: &'static str = "sharrattj/bash"; +pub const DEFAULT_BOOT_WEBC: &str = "sharrattj/bash"; //pub const DEFAULT_BOOT_WEBC: &str = "sharrattj/dash"; -pub const DEFAULT_BOOT_USES: [&'static str; 2] = ["sharrattj/coreutils", "sharrattj/catsay"]; +pub const DEFAULT_BOOT_USES: [&str; 2] = ["sharrattj/coreutils", "sharrattj/catsay"]; #[wasm_bindgen] pub fn start() -> Result<(), JsValue> { @@ -134,8 +133,6 @@ pub fn start() -> Result<(), JsValue> { tty_options, ); - let compiled_modules = Arc::new(ModuleCache::new(None, None, false)); - let location = url::Url::parse(location.as_str()).unwrap(); let mut console = if let Some(init) = location .query_pairs() @@ -143,11 +140,11 @@ pub fn start() -> Result<(), JsValue> { .next() .map(|(_, val)| val.to_string()) { - let mut console = Console::new(init.as_str(), runtime.clone(), compiled_modules); + let mut console = Console::new(init.as_str(), runtime.clone()); console = console.with_no_welcome(true); console } else { - let mut console = Console::new(DEFAULT_BOOT_WEBC, runtime.clone(), compiled_modules); + let mut console = Console::new(DEFAULT_BOOT_WEBC, runtime.clone()); console = console.with_uses(DEFAULT_BOOT_USES.iter().map(|a| a.to_string()).collect()); console }; diff --git a/lib/wasi-web/src/pool.rs b/lib/wasi-web/src/pool.rs index 8667d551618..2cae330e93a 100644 --- a/lib/wasi-web/src/pool.rs +++ b/lib/wasi-web/src/pool.rs @@ -662,7 +662,7 @@ pub fn wasm_entry_point( run(TaskWasmRunProperties { ctx, store, - result: task.result, + trigger_result: task.result, }); }; } diff --git a/lib/wasi-web/src/runtime.rs b/lib/wasi-web/src/runtime.rs index 332782d08cd..4147b1fcaa5 100644 --- a/lib/wasi-web/src/runtime.rs +++ b/lib/wasi-web/src/runtime.rs @@ -6,6 +6,7 @@ use std::time::Duration; use std::{future::Future, io, pin::Pin, sync::Arc, task::Poll}; use futures::future::BoxFuture; +use http::{HeaderMap, StatusCode}; use js_sys::Promise; use tokio::{ io::{AsyncRead, AsyncSeek, AsyncWrite}, @@ -20,7 +21,7 @@ use wasmer_wasix::{ http::{DynHttpClient, HttpRequest, HttpResponse}, os::{TtyBridge, TtyOptions}, runtime::task_manager::TaskWasm, - VirtualFile, VirtualNetworking, VirtualTaskManager, WasiRuntime, WasiThreadError, WasiTtyState, + VirtualFile, VirtualNetworking, VirtualTaskManager, WasiThreadError, WasiTtyState, }; use web_sys::WebGl2RenderingContext; @@ -416,7 +417,7 @@ impl VirtualFile for TermLog { } } -impl WasiRuntime for WebRuntime { +impl wasmer_wasix::Runtime for WebRuntime { fn networking(&self) -> &wasmer_wasix::virtual_net::DynVirtualNetworking { &self.net } @@ -483,33 +484,28 @@ struct WebHttpClient { impl WebHttpClient { async fn do_request(request: HttpRequest) -> Result { let resp = crate::common::fetch( - &request.url, - &request.method, + request.url.as_str(), + request.method.as_str(), request.options.gzip, request.options.cors_proxy, - request.headers, + &request.headers, request.body, ) .await?; - let ok = resp.ok(); let redirected = resp.redirected(); - let status = resp.status(); - let status_text = resp.status_text(); + let status = StatusCode::from_u16(resp.status())?; let data = crate::common::get_response_data(resp).await?; - let headers = Vec::new(); // FIXME: we can't implement this as the method resp.headers().keys() is missing! // how else are we going to parse the headers? + let headers = HeaderMap::new(); debug!("received {} bytes", data.len()); let resp = HttpResponse { - pos: 0, - ok, redirected, status, - status_text, headers, body: Some(data), }; diff --git a/lib/wasi/src/http/client.rs b/lib/wasi/src/http/client.rs index b8a332897a5..4c576cd76c7 100644 --- a/lib/wasi/src/http/client.rs +++ b/lib/wasi/src/http/client.rs @@ -1,6 +1,8 @@ use std::{collections::HashSet, ops::Deref, sync::Arc}; use futures::future::BoxFuture; +use http::{HeaderMap, Method, StatusCode}; +use url::Url; /// Defines http client permissions. #[derive(Clone, Debug)] @@ -47,46 +49,62 @@ pub struct HttpRequestOptions { // TODO: use types from http crate? pub struct HttpRequest { - pub url: String, - pub method: String, - pub headers: Vec<(String, String)>, + pub url: Url, + pub method: Method, + pub headers: HeaderMap, pub body: Option>, pub options: HttpRequestOptions, } impl std::fmt::Debug for HttpRequest { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let HttpRequest { + url, + method, + headers, + body, + options, + } = self; + f.debug_struct("HttpRequest") - .field("url", &self.url) - .field("method", &self.method) - .field("headers", &self.headers) - .field("body", &self.body.as_deref().map(String::from_utf8_lossy)) - .field("options", &self.options) + .field("url", &format_args!("{}", url)) + .field("method", method) + .field("headers", headers) + .field("body", &body.as_deref().map(String::from_utf8_lossy)) + .field("options", &options) .finish() } } // TODO: use types from http crate? pub struct HttpResponse { - pub pos: usize, pub body: Option>, - pub ok: bool, pub redirected: bool, - pub status: u16, - pub status_text: String, - pub headers: Vec<(String, String)>, + pub status: StatusCode, + pub headers: HeaderMap, +} + +impl HttpResponse { + pub fn is_ok(&self) -> bool { + !self.status.is_client_error() && !self.status.is_server_error() + } } impl std::fmt::Debug for HttpResponse { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let HttpResponse { + body, + redirected, + status, + headers, + } = self; + f.debug_struct("HttpResponse") - .field("pos", &self.pos) - .field("body", &self.body.as_deref().map(String::from_utf8_lossy)) - .field("ok", &self.ok) - .field("redirected", &self.redirected) - .field("status", &self.status) - .field("status_text", &self.status_text) - .field("headers", &self.headers) + .field("ok", &self.is_ok()) + .field("redirected", &redirected) + .field("status", &status) + .field("headers", &headers) + .field("body", &body.as_deref().map(String::from_utf8_lossy)) .finish() } } diff --git a/lib/wasi/src/http/client_impl.rs b/lib/wasi/src/http/client_impl.rs index 4d1b606869b..ff278e4858c 100644 --- a/lib/wasi/src/http/client_impl.rs +++ b/lib/wasi/src/http/client_impl.rs @@ -1,12 +1,13 @@ -use std::string::FromUtf8Error; use std::sync::Arc; -use crate::bindings::wasix_http_client_v1 as sys; -use crate::{capabilities::Capabilities, Runtime}; +use http::{HeaderMap, HeaderValue}; +use url::Url; use crate::{ + bindings::wasix_http_client_v1 as sys, + capabilities::Capabilities, http::{DynHttpClient, HttpClientCapabilityV1}, - WasiEnv, + Runtime, WasiEnv, }; impl std::fmt::Display for sys::Method<'_> { @@ -92,11 +93,12 @@ impl sys::WasixHttpClientV1 for WasixHttpClientImpl { .headers .into_iter() .map(|h| { - let value = String::from_utf8(h.value.to_vec())?; - Ok((h.key.to_string(), value)) + let value = HeaderValue::from_bytes(h.value).map_err(|e| e.to_string())?; + let key = + http::HeaderName::from_bytes(h.key.as_bytes()).map_err(|e| e.to_string())?; + Ok((key, value)) }) - .collect::, FromUtf8Error>>() - .map_err(|_| "non-utf8 request header")?; + .collect::>()?; // FIXME: stream body... @@ -108,9 +110,22 @@ impl sys::WasixHttpClientV1 for WasixHttpClientImpl { None => None, }; + let method = match request.method { + sys::Method::Get => http::Method::GET, + sys::Method::Head => http::Method::HEAD, + sys::Method::Post => http::Method::POST, + sys::Method::Put => http::Method::PUT, + sys::Method::Delete => http::Method::DELETE, + sys::Method::Connect => http::Method::CONNECT, + sys::Method::Options => http::Method::OPTIONS, + sys::Method::Trace => http::Method::TRACE, + sys::Method::Patch => http::Method::PATCH, + sys::Method::Other(other) => return Err(format!("Unknown method: {other}")), + }; + let req = crate::http::HttpRequest { - url: request.url.to_string(), - method: request.method.to_string(), + url: Url::parse(request.url).map_err(|e| e.to_string())?, + method, headers, body, options: crate::http::HttpRequestOptions { @@ -128,10 +143,10 @@ impl sys::WasixHttpClientV1 for WasixHttpClientImpl { let res_headers = res .headers - .into_iter() - .map(|(key, value)| sys::HeaderResult { - key, - value: value.into_bytes(), + .iter() + .map(|(name, value)| sys::HeaderResult { + key: name.to_string(), + value: value.as_bytes().to_vec(), }) .collect(); @@ -143,7 +158,7 @@ impl sys::WasixHttpClientV1 for WasixHttpClientImpl { Ok({ sys::Response { - status: res.status, + status: res.status.as_u16(), headers: res_headers, body: res_body, // TODO: provide redirect urls? diff --git a/lib/wasi/src/http/reqwest.rs b/lib/wasi/src/http/reqwest.rs index 935044b876e..f0ef6ffcd84 100644 --- a/lib/wasi/src/http/reqwest.rs +++ b/lib/wasi/src/http/reqwest.rs @@ -30,23 +30,14 @@ impl ReqwestHttpClient { .build() .context("Failed to construct http request")?; - let response = client.execute(request).await?; - - let status = response.status().as_u16(); - let status_text = response.status().as_str().to_string(); - // TODO: prevent redundant header copy. - let headers = response - .headers() - .iter() - .map(|(k, v)| (k.to_string(), v.to_str().unwrap().to_string())) - .collect(); + let mut response = client.execute(request).await?; + let headers = std::mem::take(response.headers_mut()); + + let status = response.status(); let data = response.bytes().await?.to_vec(); Ok(HttpResponse { - pos: 0usize, - ok: true, status, - status_text, redirected: false, body: Some(data), headers, diff --git a/lib/wasi/src/runtime/package_loader/builtin_loader.rs b/lib/wasi/src/runtime/package_loader/builtin_loader.rs index 20b4da26a78..c6f44d97922 100644 --- a/lib/wasi/src/runtime/package_loader/builtin_loader.rs +++ b/lib/wasi/src/runtime/package_loader/builtin_loader.rs @@ -8,6 +8,7 @@ use std::{ use anyhow::{Context, Error}; use bytes::Bytes; +use http::{HeaderMap, Method}; use tempfile::NamedTempFile; use webc::{ compat::{Container, ContainerError}, @@ -16,7 +17,7 @@ use webc::{ use crate::{ bin_factory::BinaryPackage, - http::{HttpClient, HttpRequest, HttpResponse, USER_AGENT}, + http::{HttpClient, HttpRequest, USER_AGENT}, runtime::{ package_loader::PackageLoader, resolver::{DistributionInfo, PackageSummary, Resolution, WebcHash}, @@ -99,29 +100,24 @@ impl BuiltinPackageLoader { } let request = HttpRequest { - url: dist.webc.to_string(), - method: "GET".to_string(), - headers: vec![ - ("Accept".to_string(), "application/webc".to_string()), - ("User-Agent".to_string(), USER_AGENT.to_string()), - ], + url: dist.webc.clone(), + method: Method::GET, + headers: headers(), body: None, options: Default::default(), }; - let HttpResponse { - body, - ok, - status, - status_text, - .. - } = self.client.request(request).await?; + let response = self.client.request(request).await?; - if !ok { - anyhow::bail!("{status} {status_text}"); + if !response.is_ok() { + let url = &dist.webc; + return Err(crate::runtime::resolver::utils::http_error(&response) + .context(format!("The GET request to \"{url}\" failed"))); } - let body = body.context("The response didn't contain a body")?; + let body = response + .body + .context("The response didn't contain a body")?; Ok(body.into()) } @@ -213,6 +209,13 @@ impl PackageLoader for BuiltinPackageLoader { } } +fn headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert("Accept", "application/webc".parse().unwrap()); + headers.insert("User-Agent", USER_AGENT.parse().unwrap()); + headers +} + fn discover_wasmer_dir() -> Option { // TODO: We should reuse the same logic from the wasmer CLI. std::env::var("WASMER_DIR") @@ -307,6 +310,7 @@ mod tests { use std::{collections::VecDeque, sync::Mutex}; use futures::future::BoxFuture; + use http::{HeaderMap, StatusCode}; use tempfile::TempDir; use crate::{ @@ -348,13 +352,10 @@ mod tests { async fn cache_misses_will_trigger_a_download() { let temp = TempDir::new().unwrap(); let client = Arc::new(DummyClient::with_responses([HttpResponse { - pos: 0, body: Some(PYTHON.to_vec()), - ok: true, redirected: false, - status: 200, - status_text: "OK".to_string(), - headers: Vec::new(), + status: StatusCode::OK, + headers: HeaderMap::new(), }])); let loader = BuiltinPackageLoader::new_with_client(temp.path(), client.clone()); let summary = PackageSummary { @@ -376,15 +377,11 @@ mod tests { // A HTTP request was sent let requests = client.requests.lock().unwrap(); let request = &requests[0]; - assert_eq!(request.url, summary.dist.webc.to_string()); + assert_eq!(request.url, summary.dist.webc); assert_eq!(request.method, "GET"); - assert_eq!( - request.headers, - [ - ("Accept".to_string(), "application/webc".to_string()), - ("User-Agent".to_string(), USER_AGENT.to_string()), - ] - ); + assert_eq!(request.headers.len(), 2); + assert_eq!(request.headers["Accept"], "application/webc"); + assert_eq!(request.headers["User-Agent"], USER_AGENT); // Make sure we got the right package let manifest = container.manifest(); assert_eq!(manifest.entrypoint.as_deref(), Some("python")); diff --git a/lib/wasi/src/runtime/resolver/filesystem_source.rs b/lib/wasi/src/runtime/resolver/filesystem_source.rs index 60952e33844..88bacfa5661 100644 --- a/lib/wasi/src/runtime/resolver/filesystem_source.rs +++ b/lib/wasi/src/runtime/resolver/filesystem_source.rs @@ -29,7 +29,7 @@ impl Source for FileSystemSource { let container = Container::from_disk(&path) .with_context(|| format!("Unable to parse \"{}\"", path.display()))?; - let url = crate::runtime::resolver::polyfills::url_from_file_path(&path) + let url = crate::runtime::resolver::utils::url_from_file_path(&path) .ok_or_else(|| anyhow::anyhow!("Unable to turn \"{}\" into a URL", path.display()))?; let summary = PackageSummary { diff --git a/lib/wasi/src/runtime/resolver/in_memory_source.rs b/lib/wasi/src/runtime/resolver/in_memory_source.rs index a8bb600ae1e..c8a7e34b6a0 100644 --- a/lib/wasi/src/runtime/resolver/in_memory_source.rs +++ b/lib/wasi/src/runtime/resolver/in_memory_source.rs @@ -170,7 +170,7 @@ mod tests { entrypoint: Some("bash".to_string()), }, dist: DistributionInfo { - webc: crate::runtime::resolver::polyfills::url_from_file_path( + webc: crate::runtime::resolver::utils::url_from_file_path( bash.canonicalize().unwrap() ) .unwrap(), diff --git a/lib/wasi/src/runtime/resolver/inputs.rs b/lib/wasi/src/runtime/resolver/inputs.rs index 9c236fed848..b835b6e7fac 100644 --- a/lib/wasi/src/runtime/resolver/inputs.rs +++ b/lib/wasi/src/runtime/resolver/inputs.rs @@ -139,10 +139,9 @@ impl PackageSummary { let path = path.as_ref().canonicalize()?; let container = Container::from_disk(&path)?; let webc_sha256 = WebcHash::for_file(&path)?; - let url = - crate::runtime::resolver::polyfills::url_from_file_path(&path).ok_or_else(|| { - anyhow::anyhow!("Unable to turn \"{}\" into a file:// URL", path.display()) - })?; + let url = crate::runtime::resolver::utils::url_from_file_path(&path).ok_or_else(|| { + anyhow::anyhow!("Unable to turn \"{}\" into a file:// URL", path.display()) + })?; let pkg = PackageInfo::from_manifest(container.manifest())?; let dist = DistributionInfo { diff --git a/lib/wasi/src/runtime/resolver/mod.rs b/lib/wasi/src/runtime/resolver/mod.rs index 4cb65aa0b08..6ab6a533cf0 100644 --- a/lib/wasi/src/runtime/resolver/mod.rs +++ b/lib/wasi/src/runtime/resolver/mod.rs @@ -3,9 +3,9 @@ mod in_memory_source; mod inputs; mod multi_source_registry; mod outputs; -pub(crate) mod polyfills; mod resolve; mod source; +pub(crate) mod utils; mod wapm_source; mod web_source; diff --git a/lib/wasi/src/runtime/resolver/polyfills.rs b/lib/wasi/src/runtime/resolver/utils.rs similarity index 51% rename from lib/wasi/src/runtime/resolver/polyfills.rs rename to lib/wasi/src/runtime/resolver/utils.rs index 7f7614ab6a1..521215b2cb2 100644 --- a/lib/wasi/src/runtime/resolver/polyfills.rs +++ b/lib/wasi/src/runtime/resolver/utils.rs @@ -1,7 +1,11 @@ use std::path::Path; +use anyhow::Error; +use http::{HeaderMap, StatusCode}; use url::Url; +use crate::http::{HttpResponse, USER_AGENT}; + /// Polyfill for [`Url::from_file_path()`] that works on `wasm32-unknown-unknown`. pub(crate) fn url_from_file_path(path: impl AsRef) -> Option { let path = path.as_ref(); @@ -25,6 +29,33 @@ pub(crate) fn url_from_file_path(path: impl AsRef) -> Option { buffer.parse().ok() } +pub(crate) fn webc_headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert("Accept", "application/webc".parse().unwrap()); + headers.insert("User-Agent", USER_AGENT.parse().unwrap()); + headers +} + +pub(crate) fn http_error(response: &HttpResponse) -> Error { + let status = response.status; + + if status == StatusCode::SERVICE_UNAVAILABLE { + if let Some(retry_after) = response + .headers + .get("Retry-After") + .and_then(|retry_after| retry_after.to_str().ok()) + { + tracing::debug!( + %retry_after, + "Received 503 Service Unavailable while looking up a package. The backend may still be generating the *.webc file.", + ); + return anyhow::anyhow!("{status} (Retry After: {retry_after})"); + } + } + + Error::msg(status) +} + #[cfg(test)] mod tests { use super::*; diff --git a/lib/wasi/src/runtime/resolver/wapm_source.rs b/lib/wasi/src/runtime/resolver/wapm_source.rs index d6e80c4345f..939c8957580 100644 --- a/lib/wasi/src/runtime/resolver/wapm_source.rs +++ b/lib/wasi/src/runtime/resolver/wapm_source.rs @@ -1,12 +1,13 @@ use std::sync::Arc; use anyhow::{Context, Error}; +use http::{HeaderMap, Method}; use semver::Version; use url::Url; use webc::metadata::Manifest; use crate::{ - http::{HttpClient, HttpRequest, HttpResponse}, + http::{HttpClient, HttpRequest, USER_AGENT}, runtime::resolver::{ DistributionInfo, PackageInfo, PackageSpecifier, PackageSummary, Source, WebcHash, }, @@ -52,33 +53,22 @@ impl Source for WapmSource { let body = serde_json::to_string(&body)?; let request = HttpRequest { - url: self.registry_endpoint.to_string(), - method: "POST".to_string(), + url: self.registry_endpoint.clone(), + method: Method::POST, body: Some(body.into_bytes()), - headers: vec![ - ( - "User-Agent".to_string(), - crate::http::USER_AGENT.to_string(), - ), - ("Content-Type".to_string(), "application/json".to_string()), - ], + headers: headers(), options: Default::default(), }; - let HttpResponse { - ok, - status, - status_text, - body, - .. - } = self.client.request(request).await?; + let response = self.client.request(request).await?; - if !ok { + if !response.is_ok() { let url = &self.registry_endpoint; - anyhow::bail!("\"{url}\" replied with {status} {status_text}"); + let status = response.status; + anyhow::bail!("\"{url}\" replied with {status}"); } - let body = body.unwrap_or_default(); + let body = response.body.unwrap_or_default(); let response: WapmWebQuery = serde_json::from_slice(&body).context("Unable to deserialize the response")?; @@ -101,6 +91,13 @@ impl Source for WapmSource { } } +fn headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert("Content-Type", "application/json".parse().unwrap()); + headers.insert("User-Agent", USER_AGENT.parse().unwrap()); + headers +} + fn decode_summary(pkg_version: WapmWebQueryGetPackageVersion) -> Result { let WapmWebQueryGetPackageVersion { manifest, @@ -178,9 +175,12 @@ pub struct WapmWebQueryGetPackageVersionDistribution { #[cfg(test)] mod tests { - use std::collections::HashMap; + use http::{HeaderMap, StatusCode}; - use crate::runtime::resolver::inputs::{DistributionInfo, PackageInfo}; + use crate::{ + http::HttpResponse, + runtime::resolver::inputs::{DistributionInfo, PackageInfo}, + }; use super::*; @@ -200,12 +200,11 @@ mod tests { // -H "Content-Type: application/json" \ // -X POST \ // -d '@wasmer_pack_cli_request.json' > wasmer_pack_cli_response.json - assert_eq!(request.method, "POST"); - assert_eq!(request.url, WapmSource::WAPM_PROD_ENDPOINT); - let headers: HashMap = request.headers.into_iter().collect(); - assert_eq!(headers.len(), 2); - assert_eq!(headers["User-Agent"], crate::http::USER_AGENT); - assert_eq!(headers["Content-Type"], "application/json"); + assert_eq!(request.method, http::Method::POST); + assert_eq!(request.url.as_str(), WapmSource::WAPM_PROD_ENDPOINT); + assert_eq!(request.headers.len(), 2); + assert_eq!(request.headers["User-Agent"], USER_AGENT); + assert_eq!(request.headers["Content-Type"], "application/json"); let body: serde_json::Value = serde_json::from_slice(request.body.as_deref().unwrap()).unwrap(); @@ -215,13 +214,10 @@ mod tests { Box::pin(async { Ok(HttpResponse { - pos: 0, body: Some(WASMER_PACK_CLI_RESPONSE.to_vec()), - ok: true, redirected: false, - status: 200, - status_text: "OK".to_string(), - headers: Vec::new(), + status: StatusCode::OK, + headers: HeaderMap::new(), }) }) } diff --git a/lib/wasi/src/runtime/resolver/web_source.rs b/lib/wasi/src/runtime/resolver/web_source.rs index bc715e33e30..8ab89fa2ead 100644 --- a/lib/wasi/src/runtime/resolver/web_source.rs +++ b/lib/wasi/src/runtime/resolver/web_source.rs @@ -7,13 +7,14 @@ use std::{ }; use anyhow::{Context, Error}; +use http::Method; use sha2::{Digest, Sha256}; use tempfile::NamedTempFile; use url::Url; use webc::compat::Container; use crate::{ - http::{HttpClient, HttpRequest, HttpResponse, USER_AGENT}, + http::{HttpClient, HttpRequest}, runtime::resolver::{ DistributionInfo, PackageInfo, PackageSpecifier, PackageSummary, Source, WebcHash, }, @@ -179,72 +180,57 @@ impl WebSource { async fn get_etag(&self, url: &Url) -> Result { let request = HttpRequest { - url: url.to_string(), - method: "HEAD".to_string(), - headers: headers(), + url: url.clone(), + method: Method::HEAD, + headers: super::utils::webc_headers(), body: None, options: Default::default(), }; - let HttpResponse { - ok, - status, - status_text, - headers, - .. - } = self.client.request(request).await?; - if !ok { - anyhow::bail!("HEAD request to \"{url}\" failed with {status} {status_text}"); + let response = self.client.request(request).await?; + + if !response.is_ok() { + return Err(super::utils::http_error(&response) + .context(format!("The HEAD request to \"{url}\" failed"))); } - let etag = headers - .into_iter() - .find(|(name, _)| name.to_string().to_lowercase() == "etag") - .map(|(_, value)| value) - .context("The HEAD request didn't contain an ETag header`")?; + let etag = response + .headers + .get("ETag") + .context("The HEAD request didn't contain an ETag header`")? + .to_str() + .context("The ETag wasn't valid UTF-8")?; - Ok(etag) + Ok(etag.to_string()) } async fn fetch(&self, url: &Url) -> Result<(Vec, Option), Error> { let request = HttpRequest { - url: url.to_string(), - method: "GET".to_string(), - headers: headers(), + url: url.clone(), + method: Method::GET, + headers: super::utils::webc_headers(), body: None, options: Default::default(), }; - let HttpResponse { - ok, - status, - status_text, - headers, - body, - .. - } = self.client.request(request).await?; + let response = self.client.request(request).await?; - if !ok { - anyhow::bail!("HEAD request to \"{url}\" failed with {status} {status_text}"); + if !response.is_ok() { + return Err(super::utils::http_error(&response) + .context(format!("The GET request to \"{url}\" failed"))); } - let body = body.context("Response didn't contain a body")?; + let body = response.body.context("Response didn't contain a body")?; - let etag = headers - .into_iter() - .find(|(name, _)| name.to_string().to_lowercase() == "etag") - .map(|(_, value)| value); + let etag = response + .headers + .get("ETag") + .and_then(|etag| etag.to_str().ok()) + .map(|etag| etag.to_string()); Ok((body, etag)) } } -fn headers() -> Vec<(String, String)> { - vec![ - ("Accept".to_string(), "application/webc".to_string()), - ("User-Agent".to_string(), USER_AGENT.to_string()), - ] -} - #[async_trait::async_trait] impl Source for WebSource { #[tracing::instrument(level = "debug", skip_all, fields(%package))] @@ -388,8 +374,11 @@ mod tests { use std::{collections::VecDeque, sync::Mutex}; use futures::future::BoxFuture; + use http::{header::IntoHeaderName, HeaderMap, StatusCode}; use tempfile::TempDir; + use crate::http::HttpResponse; + use super::*; const PYTHON: &[u8] = include_bytes!("../../../../c-api/examples/assets/python-0.1.0.wasmer"); @@ -428,19 +417,15 @@ mod tests { impl ResponseBuilder { pub fn new() -> Self { ResponseBuilder(HttpResponse { - pos: 0, body: None, - ok: true, redirected: false, - status: 200, - status_text: "OK".to_string(), - headers: Vec::new(), + status: StatusCode::OK, + headers: HeaderMap::new(), }) } - pub fn with_status(mut self, code: u16, text: impl Into) -> Self { + pub fn with_status(mut self, code: StatusCode) -> Self { self.0.status = code; - self.0.status_text = text.into(); self } @@ -449,12 +434,12 @@ mod tests { self } - pub fn with_etag(self, value: impl Into) -> Self { + pub fn with_etag(self, value: &str) -> Self { self.with_header("ETag", value) } - pub fn with_header(mut self, name: impl Into, value: impl Into) -> Self { - self.0.headers.push((name.into(), value.into())); + pub fn with_header(mut self, name: impl IntoHeaderName, value: &str) -> Self { + self.0.headers.insert(name, value.parse().unwrap()); self } @@ -511,7 +496,7 @@ mod tests { async fn fall_back_to_stale_cache_if_request_fails() { let temp = TempDir::new().unwrap(); let client = Arc::new(DummyClient::with_responses([ResponseBuilder::new() - .with_status(500, "Internal Server Error") + .with_status(StatusCode::INTERNAL_SERVER_ERROR) .build()])); // Add something to the cache let python_path = temp.path().join(DUMMY_URL_HASH);