Utopian CI setup phase 2: Better HTTP abstractions (#61)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
use anyhow::anyhow;
|
||||
use futures::AsyncReadExt as _;
|
||||
use http_client::{AsyncBody, HttpClient, RedirectPolicy};
|
||||
use gpui::http_client::{HttpClient, HttpResponse};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::Poll;
|
||||
@@ -13,58 +12,29 @@ extern "C" {
|
||||
fn global_fetch(input: &web_sys::Request) -> Result<js_sys::Promise, JsValue>;
|
||||
}
|
||||
|
||||
pub struct FetchHttpClient {
|
||||
user_agent: Option<http_client::http::header::HeaderValue>,
|
||||
}
|
||||
pub struct FetchHttpClient;
|
||||
|
||||
impl Default for FetchHttpClient {
|
||||
fn default() -> Self {
|
||||
Self { user_agent: None }
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "multithreaded")]
|
||||
impl FetchHttpClient {
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that the created `FetchHttpClient` is only used in a single thread environment.
|
||||
pub unsafe fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that the created `FetchHttpClient` is only used in a single thread environment.
|
||||
pub unsafe fn with_user_agent(user_agent: &str) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
user_agent: Some(http_client::http::header::HeaderValue::from_str(
|
||||
user_agent,
|
||||
)?),
|
||||
})
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "multithreaded"))]
|
||||
impl FetchHttpClient {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_user_agent(user_agent: &str) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
user_agent: Some(http_client::http::header::HeaderValue::from_str(
|
||||
user_agent,
|
||||
)?),
|
||||
})
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a `!Send` future to satisfy the `Send` bound on `BoxFuture`.
|
||||
///
|
||||
/// Safety: only valid in WASM contexts where the `FetchHttpClient` is
|
||||
/// confined to a single thread (guaranteed by the caller via unsafe
|
||||
/// constructors when `multithreaded` is enabled, or by the absence of
|
||||
/// threads when it is not).
|
||||
struct AssertSend<F>(F);
|
||||
|
||||
unsafe impl<F> Send for AssertSend<F> {}
|
||||
@@ -73,64 +43,29 @@ impl<F: Future> Future for AssertSend<F> {
|
||||
type Output = F::Output;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
|
||||
// Safety: pin projection for a single-field newtype wrapper.
|
||||
let inner = unsafe { self.map_unchecked_mut(|this| &mut this.0) };
|
||||
inner.poll(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for FetchHttpClient {
|
||||
fn user_agent(&self) -> Option<&http_client::http::header::HeaderValue> {
|
||||
self.user_agent.as_ref()
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&http_client::Url> {
|
||||
None
|
||||
}
|
||||
|
||||
fn send(
|
||||
fn get(
|
||||
&self,
|
||||
req: http_client::http::Request<AsyncBody>,
|
||||
) -> futures::future::BoxFuture<'static, anyhow::Result<http_client::http::Response<AsyncBody>>>
|
||||
{
|
||||
let (parts, body) = req.into_parts();
|
||||
|
||||
url: &str,
|
||||
follow_redirects: bool,
|
||||
) -> futures::future::BoxFuture<'static, anyhow::Result<HttpResponse>> {
|
||||
let url = url.to_string();
|
||||
Box::pin(AssertSend(async move {
|
||||
let body_bytes = read_body_to_bytes(body).await?;
|
||||
|
||||
let init = web_sys::RequestInit::new();
|
||||
init.set_method(parts.method.as_str());
|
||||
init.set_method("GET");
|
||||
|
||||
if let Some(redirect_policy) = parts.extensions.get::<RedirectPolicy>() {
|
||||
match redirect_policy {
|
||||
RedirectPolicy::NoFollow => {
|
||||
init.set_redirect(web_sys::RequestRedirect::Manual);
|
||||
}
|
||||
RedirectPolicy::FollowLimit(_) | RedirectPolicy::FollowAll => {
|
||||
init.set_redirect(web_sys::RequestRedirect::Follow);
|
||||
}
|
||||
}
|
||||
if !follow_redirects {
|
||||
init.set_redirect(web_sys::RequestRedirect::Manual);
|
||||
}
|
||||
|
||||
if let Some(ref bytes) = body_bytes {
|
||||
let uint8array = js_sys::Uint8Array::from(bytes.as_slice());
|
||||
init.set_body(uint8array.as_ref());
|
||||
}
|
||||
|
||||
let url = parts.uri.to_string();
|
||||
let request = web_sys::Request::new_with_str_and_init(&url, &init)
|
||||
.map_err(|error| anyhow!("failed to create fetch Request: {error:?}"))?;
|
||||
|
||||
let request_headers = request.headers();
|
||||
for (name, value) in &parts.headers {
|
||||
let value_str = value
|
||||
.to_str()
|
||||
.map_err(|_| anyhow!("non-ASCII header value for {name}"))?;
|
||||
request_headers
|
||||
.set(name.as_str(), value_str)
|
||||
.map_err(|error| anyhow!("failed to set header {name}: {error:?}"))?;
|
||||
}
|
||||
|
||||
let promise = global_fetch(&request)
|
||||
.map_err(|error| anyhow!("fetch threw an error: {error:?}"))?;
|
||||
let response_value = wasm_bindgen_futures::JsFuture::from(promise)
|
||||
@@ -141,35 +76,9 @@ impl HttpClient for FetchHttpClient {
|
||||
.dyn_into()
|
||||
.map_err(|error| anyhow!("fetch result is not a Response: {error:?}"))?;
|
||||
|
||||
let status = web_response.status();
|
||||
let mut builder = http_client::http::Response::builder().status(status);
|
||||
let status_code = http::StatusCode::from_u16(web_response.status())
|
||||
.map_err(|_| anyhow!("invalid status code"))?;
|
||||
|
||||
// `Headers` is a JS iterable yielding `[name, value]` pairs.
|
||||
// `js_sys::Array::from` calls `Array.from()` which accepts any iterable.
|
||||
let header_pairs = js_sys::Array::from(&web_response.headers());
|
||||
for index in 0..header_pairs.length() {
|
||||
match header_pairs.get(index).dyn_into::<js_sys::Array>() {
|
||||
Ok(pair) => match (pair.get(0).as_string(), pair.get(1).as_string()) {
|
||||
(Some(name), Some(value)) => {
|
||||
builder = builder.header(name, value);
|
||||
}
|
||||
(name, value) => {
|
||||
log::warn!(
|
||||
"skipping response header at index {index}: \
|
||||
name={name:?}, value={value:?}"
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(entry) => {
|
||||
log::warn!("skipping non-array header entry at index {index}: {entry:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The entire response body is eagerly buffered into memory via
|
||||
// `arrayBuffer()`. The Fetch API does not expose a synchronous
|
||||
// streaming interface; streaming would require `ReadableStream`
|
||||
// interop which is significantly more complex.
|
||||
let body_promise = web_response
|
||||
.array_buffer()
|
||||
.map_err(|error| anyhow!("failed to initiate response body read: {error:?}"))?;
|
||||
@@ -179,21 +88,12 @@ impl HttpClient for FetchHttpClient {
|
||||
let array_buffer: js_sys::ArrayBuffer = body_value
|
||||
.dyn_into()
|
||||
.map_err(|error| anyhow!("response body is not an ArrayBuffer: {error:?}"))?;
|
||||
let response_bytes = js_sys::Uint8Array::new(&array_buffer).to_vec();
|
||||
let body = js_sys::Uint8Array::new(&array_buffer).to_vec();
|
||||
|
||||
builder
|
||||
.body(AsyncBody::from(response_bytes))
|
||||
.map_err(|error| anyhow!(error))
|
||||
Ok(HttpResponse {
|
||||
status: status_code,
|
||||
body,
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_body_to_bytes(mut body: AsyncBody) -> anyhow::Result<Option<Vec<u8>>> {
|
||||
let mut buffer = Vec::new();
|
||||
body.read_to_end(&mut buffer).await?;
|
||||
if buffer.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(buffer))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user