fix(http): web requests (#3643)

This commit is contained in:
Aram Drevekenin 2024-10-04 18:09:06 +02:00 committed by GitHub
parent a88b34f54f
commit ba2772e31c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 125 additions and 627 deletions

617
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,10 +6,10 @@ use zellij_utils::consts::{
use zellij_utils::data::{Event, HttpVerb, SessionInfo};
use zellij_utils::errors::{prelude::*, BackgroundJobContext, ContextType};
use zellij_utils::input::layout::RunPlugin;
use zellij_utils::surf::{
http::{Method, Url},
RequestBuilder,
};
use zellij_utils::isahc::prelude::*;
use zellij_utils::isahc::AsyncReadResponseExt;
use zellij_utils::isahc::{config::RedirectPolicy, HttpClient, Request};
use std::collections::{BTreeMap, HashMap};
use std::fs;
@ -101,6 +101,12 @@ pub(crate) fn background_jobs_main(
let serialization_interval = serialization_interval.map(|s| s * 1000); // convert to
// milliseconds
let http_client = HttpClient::builder()
// TODO: timeout?
.redirect_policy(RedirectPolicy::Follow)
.build()
.ok();
loop {
let (event, mut err_ctx) = bus.recv().with_context(err_context)?;
err_ctx.add_call(ContextType::BackgroundJob((&event).into()));
@ -275,41 +281,60 @@ pub(crate) fn background_jobs_main(
BackgroundJob::WebRequest(plugin_id, client_id, url, verb, headers, body, context) => {
task::spawn({
let senders = bus.senders.clone();
let http_client = http_client.clone();
async move {
async fn web_request(
url: String,
verb: HttpVerb,
headers: BTreeMap<String, String>,
body: Vec<u8>,
http_client: HttpClient,
) -> Result<
(u16, BTreeMap<String, String>, Vec<u8>), // status_code, headers, body
zellij_utils::surf::Error,
zellij_utils::isahc::Error,
> {
let url = Url::parse(&url)?;
let http_method = match verb {
HttpVerb::Get => Method::Get,
HttpVerb::Post => Method::Post,
HttpVerb::Put => Method::Put,
HttpVerb::Delete => Method::Delete,
let mut request = match verb {
HttpVerb::Get => Request::get(url),
HttpVerb::Post => Request::post(url),
HttpVerb::Put => Request::put(url),
HttpVerb::Delete => Request::delete(url),
};
let mut req = RequestBuilder::new(http_method, url);
if !body.is_empty() {
req = req.body(body);
}
for (header, value) in headers {
req = req.header(header.as_str(), value);
request = request.header(header.as_str(), value);
}
let mut res = req.await?;
let mut res = if !body.is_empty() {
let req = request.body(body)?;
http_client.send_async(req).await?
} else {
let req = request.body(())?;
http_client.send_async(req).await?
};
let status_code = res.status();
let headers: BTreeMap<String, String> = res
.headers()
.iter()
.map(|(name, value)| (name.to_string(), value.to_string()))
.filter_map(|(name, value)| match value.to_str() {
Ok(value) => Some((name.to_string(), value.to_string())),
Err(e) => {
log::error!(
"Failed to convert header {:?} to string: {:?}",
name,
e
);
None
},
})
.collect();
let body = res.take_body().into_bytes().await?;
Ok((status_code as u16, headers, body))
let body = res.bytes().await?;
Ok((status_code.as_u16(), headers, body))
}
let Some(http_client) = http_client else {
log::error!("Cannot perform http request, likely due to a misconfigured http client");
return;
};
match web_request(url, verb, headers, body).await {
match web_request(url, verb, headers, body, http_client).await {
Ok((status, headers, body)) => {
let _ = senders.send_to_plugin(PluginInstruction::Update(vec![(
Some(plugin_id),

View file

@ -56,10 +56,8 @@ async-std = { version = "1.3.0", features = ["unstable", "attributes"] }
notify-debouncer-full = "0.1.0"
humantime = "2.1.0"
futures = "0.3.28"
surf = { version = "2.3.2", default-features = false, features = [
"curl-client",
] }
openssl-sys = { version = "0.9.93", features = ["vendored"] }
isahc = "1.7.2"
[dev-dependencies]
insta = { version = "1.6.0", features = ["backtrace"] }

View file

@ -3,15 +3,18 @@ use async_std::{
io::{ReadExt, WriteExt},
stream::StreamExt,
};
use isahc::prelude::*;
use isahc::{config::RedirectPolicy, HttpClient, Request};
use std::path::PathBuf;
use surf::Client;
use thiserror::Error;
use url::Url;
#[derive(Error, Debug)]
pub enum DownloaderError {
#[error("RequestError: {0}")]
Request(surf::Error),
Request(#[from] isahc::Error),
#[error("HttpError: {0}")]
HttpError(#[from] isahc::http::Error),
#[error("IoError: {0}")]
Io(#[source] std::io::Error),
#[error("File name cannot be found in URL: {0}")]
@ -22,14 +25,18 @@ pub enum DownloaderError {
#[derive(Debug)]
pub struct Downloader {
client: Client,
client: Option<HttpClient>,
location: PathBuf,
}
impl Default for Downloader {
fn default() -> Self {
Self {
client: surf::client().with(surf::middleware::Redirect::default()),
client: HttpClient::builder()
// TODO: timeout?
.redirect_policy(RedirectPolicy::Follow)
.build()
.ok(),
location: PathBuf::from(""),
}
}
@ -38,7 +45,11 @@ impl Default for Downloader {
impl Downloader {
pub fn new(location: PathBuf) -> Self {
Self {
client: surf::client().with(surf::middleware::Redirect::default()),
client: HttpClient::builder()
// TODO: timeout?
.redirect_policy(RedirectPolicy::Follow)
.build()
.ok(),
location,
}
}
@ -48,17 +59,19 @@ impl Downloader {
url: &str,
file_name: Option<&str>,
) -> Result<(), DownloaderError> {
let Some(client) = &self.client else {
log::error!("No Http client found, cannot perform requests - this is likely a misconfiguration of isahc::HttpClient");
return Ok(());
};
let file_name = match file_name {
Some(name) => name.to_string(),
None => self.parse_name(url)?,
};
let file_path = self.location.join(file_name.as_str());
if file_path.exists() {
log::debug!("File already exists: {:?}", file_path);
return Ok(());
}
let file_part_path = self.location.join(format!("{}.part", file_name));
let (mut target, file_part_size) = {
if file_part_path.exists() {
@ -86,16 +99,13 @@ impl Downloader {
(file_part, 0)
}
};
let res = self
.client
.get(url)
let request = Request::get(url)
.header("Content-Type", "application/octet-stream")
.header("Range", format!("bytes={}-", file_part_size))
.await
.map_err(|e| DownloaderError::Request(e))?;
let mut stream = res.bytes();
.body(())?;
let mut res = client.send_async(request).await?;
let body = res.body_mut();
let mut stream = body.bytes();
while let Some(byte) = stream.next().await {
let byte = byte.map_err(|e| DownloaderError::Io(e))?;
target
@ -113,17 +123,19 @@ impl Downloader {
Ok(())
}
pub async fn download_without_cache(url: &str) -> Result<String, DownloaderError> {
// result is the stringified body
let client = surf::client().with(surf::middleware::Redirect::default());
let res = client
.get(url)
let request = Request::get(url)
.header("Content-Type", "application/octet-stream")
.await
.map_err(|e| DownloaderError::Request(e))?;
.body(())?;
let client = HttpClient::builder()
// TODO: timeout?
.redirect_policy(RedirectPolicy::Follow)
.build()?;
let mut res = client.send_async(request).await?;
let mut downloaded_bytes: Vec<u8> = vec![];
let mut stream = res.bytes();
let body = res.body_mut();
let mut stream = body.bytes();
while let Some(byte) = stream.next().await {
let byte = byte.map_err(|e| DownloaderError::Io(e))?;
downloaded_bytes.push(byte);

View file

@ -25,9 +25,9 @@ pub mod logging; // Requires log4rs
#[cfg(not(target_family = "wasm"))]
pub use ::{
anyhow, async_channel, async_std, clap, common_path, humantime, interprocess, lazy_static,
libc, miette, nix, notify_debouncer_full, regex, serde, signal_hook, surf, tempfile, termwiz,
url, uuid, vte,
anyhow, async_channel, async_std, clap, common_path, humantime, interprocess, isahc,
lazy_static, libc, miette, nix, notify_debouncer_full, regex, serde, signal_hook, tempfile,
termwiz, url, uuid, vte,
};
pub use ::prost;