From a972467e668e5d3c6d20ce059a1fda26361a9be6 Mon Sep 17 00:00:00 2001 From: Bruno Tavares Date: Mon, 26 Jan 2026 23:33:49 -0300 Subject: [PATCH 1/2] Update libraries to latest releases The libraries on the project were outdated, and in order to integrate with recent release libraries, sych as latest `hyper` or `axum`, they needed upgrades. The major change is related to the `http 0.2 -> http 1.0` breaking change migrations, which affects the whole ecosystem as typical types such as `Uri` and `Method` are exposed to requests. Another major issue is `hyper` major release, where the `hyper::Body` became a [`Trait`]. This meant change a few type annotations and boxing values to interop with Multipart and Empty bodies under the same struct. This commit: - Upgrade all dependencies to their latest release - Adapt code on Hyper to address breaking changes - Adapt code on Actix to address newer libraries In the future, the Actix code should be revisited when a new `actix-http 5.0` gets released, as there is some pending `http 0.2 -> http 1.0` migration tidbits required for compilation. This commit also ensures all examples can compile, and were checked on a Windows and Linux box. --- ipfs-api-backend-actix/Cargo.toml | 7 +-- ipfs-api-backend-actix/src/backend.rs | 30 ++++++++++--- ipfs-api-backend-hyper/Cargo.toml | 16 ++++--- ipfs-api-backend-hyper/src/backend.rs | 57 ++++++++++++++++-------- ipfs-api-backend-hyper/src/error.rs | 6 +++ ipfs-api-examples/Cargo.toml | 6 +-- ipfs-api-prelude/Cargo.toml | 12 ++--- ipfs-api-prelude/src/backend.rs | 12 +++-- ipfs-api-prelude/src/global_opts.rs | 8 ++-- ipfs-api/Cargo.toml | 14 +++--- ipfs-api/tests/test_support/resources.rs | 6 +++ 11 files changed, 116 insertions(+), 58 deletions(-) diff --git a/ipfs-api-backend-actix/Cargo.toml b/ipfs-api-backend-actix/Cargo.toml index b16afc9..fa9e2f5 100644 --- a/ipfs-api-backend-actix/Cargo.toml +++ b/ipfs-api-backend-actix/Cargo.toml @@ -20,12 +20,13 @@ with-builder = ["ipfs-api-prelude/with-builder"] [dependencies] actix-http = "3" -actix-multipart-rfc7578 = "0.10" +actix-multipart-rfc7578 = "0.11" actix-tls = "3" awc = "3" async-trait = "0.1" bytes = "1" futures = "0.3" -http = "0.2" +http = "1" +http_02 = { version = "0.2", package = "http" } # pending https://github.com/actix/actix-web/issues/3384 ipfs-api-prelude = { version = "0.6", path = "../ipfs-api-prelude" } -thiserror = "1" +thiserror = "2" diff --git a/ipfs-api-backend-actix/src/backend.rs b/ipfs-api-backend-actix/src/backend.rs index dd4ca43..70a3d2e 100644 --- a/ipfs-api-backend-actix/src/backend.rs +++ b/ipfs-api-backend-actix/src/backend.rs @@ -18,7 +18,7 @@ use http::{ }; use ipfs_api_prelude::{ApiRequest, Backend, BoxStream, TryFromUri}; use multipart::client::multipart; -use std::time::Duration; +use std::{borrow::Borrow, time::Duration}; const ACTIX_REQUEST_TIMEOUT: Duration = Duration::from_secs(90); @@ -66,6 +66,14 @@ impl ActixBackend { } } +// Pending until https://github.com/actix/actix-web/issues/3384 +fn to_http_0_2(method: http::Method) -> http_02::Method { + match method { + http::Method::POST => http_02::Method::POST, + _ => todo!("Not used by codebase"), + } +} + #[async_trait(?Send)] impl Backend for ActixBackend { type HttpRequest = awc::SendClientRequest; @@ -91,7 +99,9 @@ impl Backend for ActixBackend { Req: ApiRequest, { let url = req.absolute_url(&self.base)?; - let req = self.client.request(Req::METHOD, url); + let req = self + .client + .request(to_http_0_2(Req::METHOD), url.to_string()); let req = if let Some((username, password)) = &self.credentials { req.basic_auth(username, password) } else { @@ -107,8 +117,11 @@ impl Backend for ActixBackend { Ok(req) } - fn get_header(res: &Self::HttpResponse, key: HeaderName) -> Option<&HeaderValue> { - res.headers().get(key) + fn get_header(res: &Self::HttpResponse, key: HeaderName) -> Option> { + // mapping is needed until https://github.com/actix/actix-web/issues/3384 is done + res.headers() + .get(key.as_str()) + .and_then(|v| HeaderValue::from_bytes(v.clone().as_bytes()).ok()) } async fn request_raw( @@ -125,7 +138,12 @@ impl Backend for ActixBackend { let body = res.body().await?; // FIXME: Actix compat with bytes 1.0 - Ok((status, body)) + Ok(( + // mapping is needed until https://github.com/actix/actix-web/issues/3384 is done + StatusCode::from_u16(status.as_u16()) + .expect("failed mapping http 0.2 to http 1.0 status code"), + body, + )) } fn response_to_byte_stream(res: Self::HttpResponse) -> BoxStream { @@ -146,7 +164,7 @@ impl Backend for ActixBackend { .err_into() .map_ok(move |mut res| { match res.status() { - StatusCode::OK => process(res).right_stream(), + http_02::StatusCode::OK => process(res).right_stream(), // If the server responded with an error status code, the body // still needs to be read so an error can be built. This block will // read the entire body stream, then immediately return an error. diff --git a/ipfs-api-backend-hyper/Cargo.toml b/ipfs-api-backend-hyper/Cargo.toml index 6f7446b..d456ed6 100644 --- a/ipfs-api-backend-hyper/Cargo.toml +++ b/ipfs-api-backend-hyper/Cargo.toml @@ -23,13 +23,15 @@ with-send-sync = ["ipfs-api-prelude/with-send-sync"] [dependencies] async-trait = "0.1" -base64 = "0.13" +base64 = "0.22" bytes = "1" futures = "0.3" -http = "0.2" -hyper = { version = "0.14", features = ["http1", "http2", "client", "tcp"] } -hyper-multipart-rfc7578 = "0.8" -hyper-rustls = { version = "0.23", features = ["rustls-native-certs"], optional = true } -hyper-tls = { version = "0.5", optional = true } +http = "1" +http-body-util = "0.1" +hyper = { version = "1.8", features = ["http1", "http2", "client"] } +hyper-util = { version = "0.1", features = ["http1", "http2", "client", "client-legacy"] } +hyper-multipart-rfc7578 = "0.9" +hyper-rustls = { version = "0.27", features = ["rustls-native-certs"], optional = true } +hyper-tls = { version = "0.6", optional = true } ipfs-api-prelude = { version = "0.6", path = "../ipfs-api-prelude" } -thiserror = "1" +thiserror = "2" diff --git a/ipfs-api-backend-hyper/src/backend.rs b/ipfs-api-backend-hyper/src/backend.rs index 0b3dce7..ff50f39 100644 --- a/ipfs-api-backend-hyper/src/backend.rs +++ b/ipfs-api-backend-hyper/src/backend.rs @@ -6,8 +6,11 @@ // copied, modified, or distributed except according to those terms. // +use std::borrow::Borrow; + use crate::error::Error; use async_trait::async_trait; +use base64::Engine as _; use bytes::Bytes; use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt}; use http::{ @@ -15,13 +18,20 @@ use http::{ uri::Scheme, StatusCode, Uri, }; -use hyper::{ - body, - client::{self, connect::Connect, Builder, HttpConnector}, +use http_body_util::{combinators::BoxBody, BodyExt}; +use hyper::body; +#[cfg(not(feature = "with-hyper-rustls"))] +#[cfg(not(feature = "with-hyper-tls"))] +use hyper_util::client::legacy::connect::HttpConnector; +use hyper_util::{ + client::legacy::{self as client, connect::Connect}, + rt::TokioExecutor, }; use ipfs_api_prelude::{ApiRequest, Backend, BoxStream, TryFromUri}; use multipart::client::multipart; +type RequestBody = BoxBody; + macro_rules! impl_default { ($http_connector:path) => { impl_default!($http_connector, <$http_connector>::new()); @@ -33,7 +43,7 @@ macro_rules! impl_default { C: Connect + Clone + Send + Sync + 'static, { base: Uri, - client: client::Client, + client: client::Client, /// Username and password credentials: Option<(String, String)>, @@ -52,7 +62,7 @@ macro_rules! impl_default { impl TryFromUri for HyperBackend<$http_connector> { fn build_with_base_uri(base: Uri) -> Self { - let client = Builder::default().build($constructor); + let client = client::Builder::new(TokioExecutor::new()).build($constructor); HyperBackend { base, @@ -104,7 +114,7 @@ impl HyperBackend { fn basic_authorization(&self) -> Option { self.credentials.as_ref().map(|(username, password)| { let credentials = format!("{}:{}", username, password); - let encoded = base64::encode(credentials); + let encoded = base64::prelude::BASE64_STANDARD.encode(credentials); format!("Basic {}", encoded) }) @@ -117,9 +127,9 @@ impl Backend for HyperBackend where C: Connect + Clone + Send + Sync + 'static, { - type HttpRequest = http::Request; + type HttpRequest = http::Request; - type HttpResponse = http::Response; + type HttpResponse = http::Response; type Error = Error; @@ -149,17 +159,21 @@ where } else { builder }; - let req = if let Some(form) = form { - form.set_body_convert::(builder) + form.set_body::(builder)? + .map(|body| body.boxed()) } else { - builder.body(hyper::Body::empty()) - }?; + builder.body( + http_body_util::Empty::new() + .map_err(|never| match never {}) + .boxed(), + )? + }; Ok(req) } - fn get_header(res: &Self::HttpResponse, key: HeaderName) -> Option<&HeaderValue> { + fn get_header(res: &Self::HttpResponse, key: HeaderName) -> Option> { res.headers().get(key) } @@ -174,13 +188,19 @@ where let req = self.build_base_request(req, form)?; let res = self.client.request(req).await?; let status = res.status(); - let body = body::to_bytes(res.into_body()).await?; + let body = res.into_body().collect().await?.to_bytes(); Ok((status, body)) } fn response_to_byte_stream(res: Self::HttpResponse) -> BoxStream { - Box::new(res.into_body().err_into()) + Box::new( + res.into_body() + .collect() + .into_stream() + .map_ok(|body| body.to_bytes()) + .err_into(), + ) } fn request_stream( @@ -202,10 +222,11 @@ where // still needs to be read so an error can be built. This block will // read the entire body stream, then immediately return an error. // - _ => body::to_bytes(res.into_body()) - .boxed() + _ => res + .into_body() + .collect() .map(|maybe_body| match maybe_body { - Ok(body) => Err(Self::process_error_from_body(body)), + Ok(body) => Err(Self::process_error_from_body(body.to_bytes())), Err(e) => Err(e.into()), }) .into_stream() diff --git a/ipfs-api-backend-hyper/src/error.rs b/ipfs-api-backend-hyper/src/error.rs index 17e42ff..d445139 100644 --- a/ipfs-api-backend-hyper/src/error.rs +++ b/ipfs-api-backend-hyper/src/error.rs @@ -16,6 +16,12 @@ pub enum Error { #[error("hyper client error `{0}`")] Client(#[from] hyper::Error), + #[error("hyper client error `{0}`")] + LegacyClient(#[from] hyper_util::client::legacy::Error), + + #[error("multipart parsing error `{0}`")] + MultipartParse(#[from] multipart::client::Error), + #[error("http error `{0}`")] Http(#[from] http::Error), diff --git a/ipfs-api-examples/Cargo.toml b/ipfs-api-examples/Cargo.toml index eef4a04..f30f8fd 100644 --- a/ipfs-api-examples/Cargo.toml +++ b/ipfs-api-examples/Cargo.toml @@ -17,10 +17,10 @@ maintenance = { status = "passively-maintained" } [features] default = ["with-hyper"] -with-hyper = ["ipfs-api-backend-hyper", "tokio/macros", "tokio/rt-multi-thread"] +with-hyper = ["ipfs-api-backend-hyper", "tokio/macros", "tokio/rt-multi-thread", "ipfs-api-backend-hyper/with-builder"] with-hyper-tls = ["with-hyper", "ipfs-api-backend-hyper/with-hyper-tls"] with-hyper-rustls = ["with-hyper", "ipfs-api-backend-hyper/with-hyper-rustls"] -with-actix = ["ipfs-api-backend-actix", "actix-rt"] +with-actix = ["ipfs-api-backend-actix", "actix-rt", "ipfs-api-backend-actix/with-builder"] [dependencies] actix-rt = { version = "2.6", optional = true } @@ -28,7 +28,7 @@ futures = "0.3" ipfs-api-backend-actix = { version = "0.7", path = "../ipfs-api-backend-actix", optional = true } ipfs-api-backend-hyper = { version = "0.6", path = "../ipfs-api-backend-hyper", optional = true } tar = "0.4" -thiserror = "1" +thiserror = "2" tokio = { version = "1", features = ["time"] } tokio-stream = { version = "0.1", features = ["time"] } tracing-subscriber = { version = "0.3", features = ["fmt"] } diff --git a/ipfs-api-prelude/Cargo.toml b/ipfs-api-prelude/Cargo.toml index 3dafc81..88a9906 100644 --- a/ipfs-api-prelude/Cargo.toml +++ b/ipfs-api-prelude/Cargo.toml @@ -23,20 +23,20 @@ with-send-sync = [] async-trait = "0.1" bytes = "1" cfg-if = "1" -common-multipart-rfc7578 = "0.6" -dirs = "4" +common-multipart-rfc7578 = "0.7" +dirs = "6" futures = "0.3" -http = "0.2" -multiaddr = "0.17" +http = "1" +multiaddr = "0.18" multibase = "0.9" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_urlencoded = "0.7" -thiserror = "1" +thiserror = "2" tokio = "1" tokio-util = { version = "0.7", features = ["codec"] } tracing = "0.1" -typed-builder = { version = "0.10", optional = true } +typed-builder = { version = "0.23", optional = true } walkdir = "2.3" [dev-dependencies] diff --git a/ipfs-api-prelude/src/backend.rs b/ipfs-api-prelude/src/backend.rs index 8d96a31..040a16a 100644 --- a/ipfs-api-prelude/src/backend.rs +++ b/ipfs-api-prelude/src/backend.rs @@ -20,7 +20,10 @@ use http::{ StatusCode, }; use serde::Deserialize; -use std::fmt::{Debug, Display}; +use std::{ + borrow::Borrow, + fmt::{Debug, Display}, +}; use tokio_util::codec::{Decoder, FramedRead}; cfg_if::cfg_if! { @@ -64,7 +67,8 @@ pub trait Backend { /// Get the value of a header from an HTTP response. /// - fn get_header(res: &Self::HttpResponse, key: HeaderName) -> Option<&HeaderValue>; + // TODO: replace `impl Borrow` with `&` after https://github.com/actix/actix-web/issues/3384 is done + fn get_header(res: &Self::HttpResponse, key: HeaderName) -> Option>; /// Generates a request, and returns the unprocessed response future. /// @@ -214,11 +218,11 @@ pub trait Backend { // is used to indicate that there was an error while streaming // data with Ipfs. // - if trailer == X_STREAM_ERROR_KEY { + if trailer.borrow() == X_STREAM_ERROR_KEY { true } else { let err = crate::Error::UnrecognizedTrailerHeader( - String::from_utf8_lossy(trailer.as_ref()).into(), + String::from_utf8_lossy(trailer.borrow().as_bytes()).into(), ); // There was an unrecognized trailer value. If that is the case, diff --git a/ipfs-api-prelude/src/global_opts.rs b/ipfs-api-prelude/src/global_opts.rs index 123611f..2e5798d 100644 --- a/ipfs-api-prelude/src/global_opts.rs +++ b/ipfs-api-prelude/src/global_opts.rs @@ -11,7 +11,7 @@ use async_trait::async_trait; use bytes::Bytes; use common_multipart_rfc7578::client::multipart; use serde::{Serialize, Serializer}; -use std::time::Duration; +use std::{borrow::Borrow, time::Duration}; /// Options valid on any IPFS Api request /// @@ -85,7 +85,7 @@ impl BackendWithGlobalOptions { } } - fn combine(&self, req: Req) -> OptCombiner + fn combine(&'_ self, req: Req) -> OptCombiner<'_, Req> where Req: ApiRequest, { @@ -136,7 +136,7 @@ impl Backend for BackendWithGlobalOptions { fn get_header( res: &Self::HttpResponse, key: http::header::HeaderName, - ) -> Option<&http::HeaderValue> { + ) -> Option> { Back::get_header(res, key) } @@ -198,7 +198,7 @@ impl Backend for BackendWithGlobalOptions { fn get_header( res: &Self::HttpResponse, key: http::header::HeaderName, - ) -> Option<&http::HeaderValue> { + ) -> Option> { Back::get_header(res, key) } diff --git a/ipfs-api/Cargo.toml b/ipfs-api/Cargo.toml index ce8ba8f..ba1502b 100644 --- a/ipfs-api/Cargo.toml +++ b/ipfs-api/Cargo.toml @@ -31,16 +31,16 @@ actix-rt = "2.5" cfg-if = "1" futures = "0.3" ipfs-api-versions = { version = "0.1", path = "../ipfs-api-versions" } -passivized_docker_engine_client = "0.0.8" -passivized_htpasswd = "0.0.5" -reqwest = "0.11" +passivized_docker_engine_client = "0.0.9" +passivized_htpasswd = "0.0.6" +reqwest = "0.13" tempfile = "3.3" -test-case = "2.2" -thiserror = "1.0" +test-case = "3.3" +thiserror = "2.0" tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] } # Only when testing with-hyper -hyper = "0.14" +hyper = "1.8" # Only when testing with-hyper-tls -hyper-tls = "0.5" +hyper-tls = "0.6" diff --git a/ipfs-api/tests/test_support/resources.rs b/ipfs-api/tests/test_support/resources.rs index b25c43c..e59574b 100644 --- a/ipfs-api/tests/test_support/resources.rs +++ b/ipfs-api/tests/test_support/resources.rs @@ -43,6 +43,12 @@ async fn read_default_conf_template() -> Result { Ok(tokio::fs::read_to_string(path).await?) } +#[cfg(not(unix))] +pub fn set_config_permissions(_: &Path) -> Result<(), std::io::Error> { + Ok(()) +} + +#[cfg(unix)] pub fn set_config_permissions(path: &Path) -> Result<(), std::io::Error> { use std::os::unix::fs::PermissionsExt; From af35ebaa916c735be8568e223b59ecf35782fd99 Mon Sep 17 00:00:00 2001 From: Bruno Tavares Date: Wed, 4 Feb 2026 19:24:55 -0300 Subject: [PATCH 2/2] Fix compilation when TLS features are enabled --- ipfs-api-backend-hyper/Cargo.toml | 2 +- ipfs-api-backend-hyper/src/backend.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ipfs-api-backend-hyper/Cargo.toml b/ipfs-api-backend-hyper/Cargo.toml index d456ed6..3ba7262 100644 --- a/ipfs-api-backend-hyper/Cargo.toml +++ b/ipfs-api-backend-hyper/Cargo.toml @@ -31,7 +31,7 @@ http-body-util = "0.1" hyper = { version = "1.8", features = ["http1", "http2", "client"] } hyper-util = { version = "0.1", features = ["http1", "http2", "client", "client-legacy"] } hyper-multipart-rfc7578 = "0.9" -hyper-rustls = { version = "0.27", features = ["rustls-native-certs"], optional = true } +hyper-rustls = { version = "0.27", features = ["rustls-native-certs", "http2"], optional = true } hyper-tls = { version = "0.6", optional = true } ipfs-api-prelude = { version = "0.6", path = "../ipfs-api-prelude" } thiserror = "2" diff --git a/ipfs-api-backend-hyper/src/backend.rs b/ipfs-api-backend-hyper/src/backend.rs index ff50f39..1cd6214 100644 --- a/ipfs-api-backend-hyper/src/backend.rs +++ b/ipfs-api-backend-hyper/src/backend.rs @@ -20,8 +20,6 @@ use http::{ }; use http_body_util::{combinators::BoxBody, BodyExt}; use hyper::body; -#[cfg(not(feature = "with-hyper-rustls"))] -#[cfg(not(feature = "with-hyper-tls"))] use hyper_util::client::legacy::connect::HttpConnector; use hyper_util::{ client::legacy::{self as client, connect::Connect}, @@ -93,8 +91,9 @@ impl_default!( hyper_rustls::HttpsConnector, hyper_rustls::HttpsConnectorBuilder::new() .with_native_roots() + .expect("Could not start native TLS roots") .https_or_http() - .enable_http1() + .enable_all_versions() .build() );