Repository: informalsystems/tendermint-rs (language: rust)

Issue:
RUSTSEC-2023-0052: webpki: CPU denial of service in certificate path building

> webpki: CPU denial of service in certificate path building

| Details             |                                                |
| ------------------- | ---------------------------------------------- |
| Package             | `webpki`                      |
| Version             | `0.22.0`                   |
| Date                | 2023-08-22                         |

When this crate is given a pathological certificate chain to validate, it will
spend CPU time exponential with the number of candidate certificates at each
step of path building.

Both TLS clients and TLS servers that accept client certificate are affected.

This was previously reported in
&lt;https://github.com/briansmith/webpki/issues/69&gt; and re-reported recently
by Luke Malinowski.

`rustls-webpki` is a fork of this crate which contains a fix for this issue
and is actively maintained.

See [advisory page](https://rustsec.org/advisories/RUSTSEC-2023-0052.html) for additional details.



Discussion / hints:
It seems like cutting out `hyper-proxy` is the most expedient way to solve this. I'll look into available alternatives. Any suggestions on this?
`webpki` v0.22.1 now contains a fix: https://rustsec.org/advisories/RUSTSEC-2023-0052.html
> `webpki` v0.22.1 now contains a fix: https://rustsec.org/advisories/RUSTSEC-2023-0052.html

I see two crates through which this workspace depends on `webpki` 0.21.x:  `hyper-proxy` and an outdated version of `hyper-rustls`. It should be easy to update the latter, but `hyper-proxy` does not look like it will get updated any time soon. 
If you update to `webpki` v0.22.1, the fix should work with `hyper-proxy`
> If you update to `webpki` v0.22.1, the fix should work with `hyper-proxy`

Can you provide a precise way to do it?
`cargo update -p webpki@0.22.1` does not fail for me, but does not update the `webpki` dependencies either. I expected it couldn't before trying, because `hyper-proxy` is on `webpki = ~0.21`.
#1346 provides a surgical approach, by cutting out all proxy support.

The other alternatives to resolve this that I see:

- Switch the network crypto to OpenSSL and `native-tls`, bringing in vendoring issues with a platform library.
- Find or write a replacement for `hyper-proxy`.
- Live with the vulnerability, noting the limited ways in which it can be triggered (HTTP client for RPC connecting through a malicious proxy).
Oh sorry, I missed that `hyper-proxy` was a (minor) version behind on `webpki`
Thanks for the fix!

Would it be possible to cut a new `0.29.x` release for us still living in the TM 0.34 world when this PR gets merged?
HTTP CONNECT support seems doable in house by following [the doc](https://docs.rs/hyper/latest/hyper/upgrade/index.html) on protocol upgrades in `hyper`.
> Thanks for the fix!

Well... we are still debating whether removing all proxy support (which was once a requested feature) is an acceptable price for this.
FWIW we no longer run any apps which require RPC through a proxy, so if you want to rip it out that's fine with us
I have explored rewriting a proxy connector, but ran into connection lifecycle issues that are rather hard to debug, and the available examples from upstream do not fit our usage very well.
Somewhat more long-term, `hyper::Client` would no longer be available with the next major release of `hyper`.
It's better to switch to `reqwest` as the high-level HTTP client, since it also has proxy support built in.

Fix patch:
diff --git a/.changelog/unreleased/breaking-changes/1362-rpc-by-reqwest.md b/.changelog/unreleased/breaking-changes/1362-rpc-by-reqwest.md
new file mode 100644
index 000000000..f9920f46b
--- /dev/null
+++ b/.changelog/unreleased/breaking-changes/1362-rpc-by-reqwest.md
@@ -0,0 +1,10 @@
+- `[tendermint-rpc]` Changed `ErrorDetail` variants
+  ([\#1362](https://github.com/informalsystems/tendermint-rs/pull/1362)):
+  * Removed the `Hyper` and `InvalidUri` variants.
+  * The `Http` variant now has `Error` from `reqwest` as the source.
+  * Added the `InvalidProxy` variant.
+  * The `tungstenite` dependency exposed through its `Error` type in
+    WebSocket-related variants has been updated to version 0.20.x.
+- `[tendermint-rpc]` Removed a `TryFrom<HttpClientUrl>` conversion for
+  `hyper::Uri` as hyper is no longer a direct dependency
+  ([\#1362](https://github.com/informalsystems/tendermint-rs/pull/1362)).
diff --git a/.changelog/unreleased/security/1342-rpc-by-reqwest.md b/.changelog/unreleased/security/1342-rpc-by-reqwest.md
new file mode 100644
index 000000000..b34eee927
--- /dev/null
+++ b/.changelog/unreleased/security/1342-rpc-by-reqwest.md
@@ -0,0 +1,3 @@
+- `[tendermint-rpc]` Address the RUSTSEC-2023-0052 vulnerability by dropping
+  dependency on `hyper-proxy` and changing the HTTP client to use `reqwest`
+  ([\#1342](https://github.com/informalsystems/tendermint-rs/issues/1342)).
diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml
index f90dcac10..fd1f339e1 100644
--- a/rpc/Cargo.toml
+++ b/rpc/Cargo.toml
@@ -38,11 +38,7 @@ cli = [
 ]
 http-client = [
   "futures",
-  "http",
-  "hyper",
-  "hyper-proxy",
-  "hyper-rustls",
-  "tokio/fs",
+  "reqwest",
   "tokio/macros",
   "tracing"
 ]
@@ -50,7 +46,6 @@ secp256k1 = [ "tendermint/secp256k1" ]
 websocket-client = [
   "async-tungstenite",
   "futures",
-  "http",
   "tokio/rt-multi-thread",
   "tokio/fs",
   "tokio/macros",
@@ -83,17 +78,15 @@ subtle = { version = "2", default-features = false }
 semver = { version = "1.0", default-features = false }
 
 # Optional dependencies
-async-tungstenite = { version = "0.20", default-features = false, features = ["tokio-runtime", "tokio-rustls-native-certs"], optional = true }
+async-tungstenite = { version = "0.23", default-features = false, features = ["tokio-runtime", "tokio-rustls-native-certs"], optional = true }
 futures = { version = "0.3", optional = true, default-features = false }
-http = { version = "0.2", optional = true, default-features = false }
-hyper = { version = "0.14", optional = true, default-features = false, features = ["client", "http1", "http2"] }
-hyper-proxy = { version = "0.9.1", optional = true, default-features = false, features = ["rustls"] }
-hyper-rustls = { version = "0.22.1", optional = true, default-features = false, features = ["rustls-native-certs", "webpki-roots", "tokio-runtime"] }
+reqwest = { version = "0.11.20", optional = true, default-features = false, features = ["rustls-tls-native-roots"] }
 structopt = { version = "0.3", optional = true, default-features = false }
 tokio = { version = "1.0", optional = true, default-features = false, features = ["rt-multi-thread"] }
 tracing = { version = "0.1", optional = true, default-features = false }
 tracing-subscriber = { version = "0.2", optional = true, default-features = false, features = ["fmt"] }
 
 [dev-dependencies]
+http = { version = "0.2", default-features = false }
 lazy_static = { version = "1.4.0", default-features = false }
 tokio-test = { version = "0.4", default-features = false }
diff --git a/rpc/src/client/transport.rs b/rpc/src/client/transport.rs
index f8859696b..3eb72ee5f 100644
--- a/rpc/src/client/transport.rs
+++ b/rpc/src/client/transport.rs
@@ -8,8 +8,16 @@ macro_rules! perform_with_compat {
     ($self:expr, $request:expr) => {{
         let request = $request;
         match $self.compat {
-            CompatMode::V0_37 => $self.perform(request).await,
-            CompatMode::V0_34 => $self.perform_v0_34(request).await,
+            CompatMode::V0_37 => {
+                $self
+                    .perform_with_dialect(request, crate::dialect::v0_37::Dialect)
+                    .await
+            },
+            CompatMode::V0_34 => {
+                $self
+                    .perform_with_dialect(request, crate::dialect::v0_34::Dialect)
+                    .await
+            },
         }
     }};
 }
diff --git a/rpc/src/client/transport/auth.rs b/rpc/src/client/transport/auth.rs
index 4d6734ff5..28176a561 100644
--- a/rpc/src/client/transport/auth.rs
+++ b/rpc/src/client/transport/auth.rs
@@ -5,8 +5,8 @@
 use alloc::string::{String, ToString};
 use core::fmt;
 
-use http::Uri;
 use subtle_encoding::base64;
+use url::Url;
 
 /// An HTTP authorization.
 ///
@@ -28,10 +28,10 @@ impl fmt::Display for Authorization {
 ///
 /// This authorization can then be supplied to the RPC server via
 /// the `Authorization` HTTP header.
-pub fn authorize(uri: &Uri) -> Option<Authorization> {
-    let authority = uri.authority()?;
+pub fn authorize(url: &Url) -> Option<Authorization> {
+    let authority = url.authority();
 
-    if let Some((userpass, _)) = authority.as_str().split_once('@') {
+    if let Some((userpass, _)) = authority.split_once('@') {
         let bytes = base64::encode(userpass);
         let credentials = String::from_utf8_lossy(bytes.as_slice());
         Some(Authorization::Basic(credentials.to_string()))
@@ -42,28 +42,24 @@ pub fn authorize(uri: &Uri) -> Option<Authorization> {
 
 #[cfg(test)]
 mod tests {
-    use core::str::FromStr;
-
-    use http::Uri;
-
     use super::*;
 
     #[test]
     fn extract_auth_absent() {
-        let uri = Uri::from_str("http://example.com").unwrap();
+        let uri = "http://example.com".parse().unwrap();
         assert_eq!(authorize(&uri), None);
     }
 
     #[test]
     fn extract_auth_username_only() {
-        let uri = Uri::from_str("http://toto@example.com").unwrap();
+        let uri = "http://toto@example.com".parse().unwrap();
         let base64 = "dG90bw==".to_string();
         assert_eq!(authorize(&uri), Some(Authorization::Basic(base64)));
     }
 
     #[test]
     fn extract_auth_username_password() {
-        let uri = Uri::from_str("http://toto:tata@example.com").unwrap();
+        let uri = "http://toto:tata@example.com".parse().unwrap();
         let base64 = "dG90bzp0YXRh".to_string();
         assert_eq!(authorize(&uri), Some(Authorization::Basic(base64)));
     }
diff --git a/rpc/src/client/transport/http.rs b/rpc/src/client/transport/http.rs
index ceaa427cd..6bb8f092c 100644
--- a/rpc/src/client/transport/http.rs
+++ b/rpc/src/client/transport/http.rs
@@ -6,18 +6,25 @@ use core::{
 };
 
 use async_trait::async_trait;
+use reqwest::{header, Proxy};
 
 use tendermint::{block::Height, evidence::Evidence, Hash};
 use tendermint_config::net;
 
+use super::auth;
 use crate::prelude::*;
 use crate::{
     client::{Client, CompatMode},
-    dialect, endpoint,
+    dialect::{v0_34, Dialect, LatestDialect},
+    endpoint,
     query::Query,
+    request::RequestMessage,
+    response::Response,
     Error, Order, Scheme, SimpleRequest, Url,
 };
 
+const USER_AGENT: &str = concat!("tendermint.rs/", env!("CARGO_PKG_VERSION"));
+
 /// A JSON-RPC/HTTP Tendermint RPC client (implements [`crate::Client`]).
 ///
 /// Supports both HTTP and HTTPS connections to Tendermint RPC endpoints, and
@@ -46,7 +53,8 @@ use crate::{
 /// ```
 #[derive(Debug, Clone)]
 pub struct HttpClient {
-    inner: sealed::HttpClient,
+    inner: reqwest::Client,
+    url: reqwest::Url,
     compat: CompatMode,
 }
 
@@ -79,27 +87,23 @@ impl Builder {
 
     /// Try to create a client with the options specified for this builder.
     pub fn build(self) -> Result<HttpClient, Error> {
-        match self.proxy_url {
-            None => Ok(HttpClient {
-                inner: if self.url.0.is_secure() {
-                    sealed::HttpClient::new_https(self.url.try_into()?)
-                } else {
-                    sealed::HttpClient::new_http(self.url.try_into()?)
-                },
-                compat: self.compat,
-            }),
-            Some(proxy_url) => Ok(HttpClient {
-                inner: if proxy_url.0.is_secure() {
-                    sealed::HttpClient::new_https_proxy(
-                        self.url.try_into()?,
-                        proxy_url.try_into()?,
-                    )?
+        let builder = reqwest::ClientBuilder::new().user_agent(USER_AGENT);
+        let inner = match self.proxy_url {
+            None => builder.build().map_err(Error::http)?,
+            Some(proxy_url) => {
+                let proxy = if self.url.0.is_secure() {
+                    Proxy::https(reqwest::Url::from(proxy_url.0)).map_err(Error::invalid_proxy)?
                 } else {
-                    sealed::HttpClient::new_http_proxy(self.url.try_into()?, proxy_url.try_into()?)?
-                },
-                compat: self.compat,
-            }),
-        }
+                    Proxy::http(reqwest::Url::from(proxy_url.0)).map_err(Error::invalid_proxy)?
+                };
+                builder.proxy(proxy).build().map_err(Error::http)?
+            },
+        };
+        Ok(HttpClient {
+            inner,
+            url: self.url.into(),
+            compat: self.compat,
+        })
     }
 }
 
@@ -111,14 +115,7 @@ impl HttpClient {
         U: TryInto<HttpClientUrl, Error = Error>,
     {
         let url = url.try_into()?;
-        Ok(Self {
-            inner: if url.0.is_secure() {
-                sealed::HttpClient::new_https(url.try_into()?)
-            } else {
-                sealed::HttpClient::new_http(url.try_into()?)
-            },
-            compat: Default::default(),
-        })
+        Self::builder(url).build()
     }
 
     /// Construct a new Tendermint RPC HTTP/S client connecting to the given
@@ -157,11 +154,40 @@ impl HttpClient {
         self.compat = compat;
     }
 
-    async fn perform_v0_34<R>(&self, request: R) -> Result<R::Output, Error>
+    fn build_request<R>(&self, request: R) -> Result<reqwest::Request, Error>
+    where
+        R: RequestMessage,
+    {
+        let request_body = request.into_json();
+
+        tracing::trace!("outgoing request: {}", request_body);
+
+        let mut builder = self
+            .inner
+            .post(self.url.clone())
+            .header(header::CONTENT_TYPE, "application/json")
+            .body(request_body.into_bytes());
+
+        if let Some(auth) = auth::authorize(&self.url) {
+            builder = builder.header(header::AUTHORIZATION, auth.to_string());
+        }
+
+        builder.build().map_err(Error::http)
+    }
+
+    async fn perform_with_dialect<R, S>(&self, request: R, _dialect: S) -> Result<R::Output, Error>
     where
-        R: SimpleRequest<dialect::v0_34::Dialect>,
+        R: SimpleRequest<S>,
+        S: Dialect,
     {
-        self.inner.perform(request).await
+        let request = self.build_request(request)?;
+        let response = self.inner.execute(request).await.map_err(Error::http)?;
+        let response_body = response.bytes().await.map_err(Error::http)?;
+        tracing::trace!(
+            "incoming response: {}",
+            String::from_utf8_lossy(&response_body)
+        );
+        R::Response::from_string(&response_body).map(Into::into)
     }
 }
 
@@ -171,7 +197,7 @@ impl Client for HttpClient {
     where
         R: SimpleRequest,
     {
-        self.inner.perform(request).await
+        self.perform_with_dialect(request, LatestDialect).await
     }
 
     async fn block_results<H>(&self, height: H) -> Result<endpoint::block_results::Response, Error>
@@ -196,7 +222,7 @@ impl Client for HttpClient {
                 // Back-fill with a request to /block endpoint and
                 // taking just the header from the response.
                 let resp = self
-                    .perform_v0_34(endpoint::block::Request::new(height))
+                    .perform_with_dialect(endpoint::block::Request::new(height), v0_34::Dialect)
                     .await?;
                 Ok(resp.into())
             },
@@ -216,7 +242,10 @@ impl Client for HttpClient {
                 // Back-fill with a request to /block_by_hash endpoint and
                 // taking just the header from the response.
                 let resp = self
-                    .perform_v0_34(endpoint::block_by_hash::Request::new(hash))
+                    .perform_with_dialect(
+                        endpoint::block_by_hash::Request::new(hash),
+                        v0_34::Dialect,
+                    )
                     .await?;
                 Ok(resp.into())
             },
@@ -228,7 +257,7 @@ impl Client for HttpClient {
         match self.compat {
             CompatMode::V0_37 => self.perform(endpoint::evidence::Request::new(e)).await,
             CompatMode::V0_34 => {
-                self.perform_v0_34(endpoint::evidence::Request::new(e))
+                self.perform_with_dialect(endpoint::evidence::Request::new(e), v0_34::Dialect)
                     .await
             },
         }
@@ -318,168 +347,9 @@ impl From<HttpClientUrl> for Url {
     }
 }
 
-impl TryFrom<HttpClientUrl> for hyper::Uri {
-    type Error = Error;
-
-    fn try_from(value: HttpClientUrl) -> Result<Self, Error> {
-        value.0.to_string().parse().map_err(Error::invalid_uri)
-    }
-}
-
-mod sealed {
-    use std::io::Read;
-
-    use http::header::AUTHORIZATION;
-    use hyper::{
-        body::Buf,
-        client::{connect::Connect, HttpConnector},
-        header, Uri,
-    };
-    use hyper_proxy::{Intercept, Proxy, ProxyConnector};
-    use hyper_rustls::HttpsConnector;
-
-    use crate::prelude::*;
-    use crate::{
-        client::transport::auth::authorize, dialect::Dialect, Error, Response, SimpleRequest,
-    };
-
-    /// A wrapper for a `hyper`-based client, generic over the connector type.
-    #[derive(Debug, Clone)]
-    pub struct HyperClient<C> {
-        uri: Uri,
-        inner: hyper::Client<C>,
-    }
-
-    impl<C> HyperClient<C> {
-        pub fn new(uri: Uri, inner: hyper::Client<C>) -> Self {
-            Self { uri, inner }
-        }
-    }
-
-    impl<C> HyperClient<C>
-    where
-        C: Connect + Clone + Send + Sync + 'static,
-    {
-        pub async fn perform<R, S>(&self, request: R) -> Result<R::Output, Error>
-        where
-            R: SimpleRequest<S>,
-            S: Dialect,
-        {
-            let request = self.build_request(request)?;
-            let response = self.inner.request(request).await.map_err(Error::hyper)?;
-            let response_body = response_to_string(response).await?;
-            tracing::debug!("Incoming response: {}", response_body);
-            R::Response::from_string(&response_body).map(Into::into)
-        }
-    }
-
-    impl<C> HyperClient<C> {
-        /// Build a request using the given Tendermint RPC request.
-        pub fn build_request<R, S>(&self, request: R) -> Result<hyper::Request<hyper::Body>, Error>
-        where
-            R: SimpleRequest<S>,
-            S: Dialect,
-        {
-            let request_body = request.into_json();
-
-            tracing::debug!("Outgoing request: {}", request_body);
-
-            let mut request = hyper::Request::builder()
-                .method("POST")
-                .uri(&self.uri)
-                .body(hyper::Body::from(request_body.into_bytes()))
-                .map_err(Error::http)?;
-
-            {
-                let headers = request.headers_mut();
-                headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap());
-                headers.insert(
-                    header::USER_AGENT,
-                    format!("tendermint.rs/{}", env!("CARGO_PKG_VERSION"))
-                        .parse()
-                        .unwrap(),
-                );
-
-                if let Some(auth) = authorize(&self.uri) {
-                    headers.insert(AUTHORIZATION, auth.to_string().parse().unwrap());
-                }
-            }
-
-            Ok(request)
-        }
-    }
-
-    /// We offer several variations of `hyper`-based client.
-    ///
-    /// Here we erase the type signature of the underlying `hyper`-based
-    /// client, allowing the higher-level HTTP client to operate via HTTP or
-    /// HTTPS, and with or without a proxy.
-    #[derive(Debug, Clone)]
-    pub enum HttpClient {
-        Http(HyperClient<HttpConnector>),
-        Https(HyperClient<HttpsConnector<HttpConnector>>),
-        HttpProxy(HyperClient<ProxyConnector<HttpConnector>>),
-        HttpsProxy(HyperClient<ProxyConnector<HttpsConnector<HttpConnector>>>),
-    }
-
-    impl HttpClient {
-        pub fn new_http(uri: Uri) -> Self {
-            Self::Http(HyperClient::new(uri, hyper::Client::new()))
-        }
-
-        pub fn new_https(uri: Uri) -> Self {
-            Self::Https(HyperClient::new(
-                uri,
-                hyper::Client::builder().build(HttpsConnector::with_native_roots()),
-            ))
-        }
-
-        pub fn new_http_proxy(uri: Uri, proxy_uri: Uri) -> Result<Self, Error> {
-            let proxy = Proxy::new(Intercept::All, proxy_uri);
-            let proxy_connector =
-                ProxyConnector::from_proxy(HttpConnector::new(), proxy).map_err(Error::io)?;
-            Ok(Self::HttpProxy(HyperClient::new(
-                uri,
-                hyper::Client::builder().build(proxy_connector),
-            )))
-        }
-
-        pub fn new_https_proxy(uri: Uri, proxy_uri: Uri) -> Result<Self, Error> {
-            let proxy = Proxy::new(Intercept::All, proxy_uri);
-            let proxy_connector =
-                ProxyConnector::from_proxy(HttpsConnector::with_native_roots(), proxy)
-                    .map_err(Error::io)?;
-
-            Ok(Self::HttpsProxy(HyperClient::new(
-                uri,
-                hyper::Client::builder().build(proxy_connector),
-            )))
-        }
-
-        pub async fn perform<R, S>(&self, request: R) -> Result<R::Output, Error>
-        where
-            R: SimpleRequest<S>,
-            S: Dialect,
-        {
-            match self {
-                HttpClient::Http(c) => c.perform(request).await,
-                HttpClient::Https(c) => c.perform(request).await,
-                HttpClient::HttpProxy(c) => c.perform(request).await,
-                HttpClient::HttpsProxy(c) => c.perform(request).await,
-            }
-        }
-    }
-
-    async fn response_to_string(response: hyper::Response<hyper::Body>) -> Result<String, Error> {
-        let mut response_body = String::new();
-        hyper::body::aggregate(response.into_body())
-            .await
-            .map_err(Error::hyper)?
-            .reader()
-            .read_to_string(&mut response_body)
-            .map_err(Error::io)?;
-
-        Ok(response_body)
+impl From<HttpClientUrl> for url::Url {
+    fn from(url: HttpClientUrl) -> Self {
+        url.0.into()
     }
 }
 
@@ -487,14 +357,13 @@ mod sealed {
 mod tests {
     use core::str::FromStr;
 
-    use http::{header::AUTHORIZATION, Request, Uri};
-    use hyper::Body;
+    use reqwest::{header::AUTHORIZATION, Request};
 
-    use super::sealed::HyperClient;
-    use crate::dialect::LatestDialect;
+    use super::HttpClient;
     use crate::endpoint::abci_info;
+    use crate::Url;
 
-    fn authorization(req: &Request<Body>) -> Option<&str> {
+    fn authorization(req: &Request) -> Option<&str> {
         req.headers()
             .get(AUTHORIZATION)
             .map(|h| h.to_str().unwrap())
@@ -502,22 +371,18 @@ mod tests {
 
     #[test]
     fn without_basic_auth() {
-        let uri = Uri::from_str("http://example.com").unwrap();
-        let inner = hyper::Client::new();
-        let client = HyperClient::new(uri, inner);
-        let req =
-            HyperClient::build_request::<_, LatestDialect>(&client, abci_info::Request).unwrap();
+        let url = Url::from_str("http://example.com").unwrap();
+        let client = HttpClient::new(url).unwrap();
+        let req = HttpClient::build_request(&client, abci_info::Request).unwrap();
 
         assert_eq!(authorization(&req), None);
     }
 
     #[test]
     fn with_basic_auth() {
-        let uri = Uri::from_str("http://toto:tata@example.com").unwrap();
-        let inner = hyper::Client::new();
-        let client = HyperClient::new(uri, inner);
-        let req =
-            HyperClient::build_request::<_, LatestDialect>(&client, abci_info::Request).unwrap();
+        let url = Url::from_str("http://toto:tata@example.com").unwrap();
+        let client = HttpClient::new(url).unwrap();
+        let req = HttpClient::build_request(&client, abci_info::Request).unwrap();
 
         assert_eq!(authorization(&req), Some("Basic dG90bzp0YXRh"));
     }
diff --git a/rpc/src/client/transport/websocket.rs b/rpc/src/client/transport/websocket.rs
index 01ec8950d..ce3738f63 100644
--- a/rpc/src/client/transport/websocket.rs
+++ b/rpc/src/client/transport/websocket.rs
@@ -25,7 +25,6 @@ use tendermint::{block::Height, Hash};
 use tendermint_config::net;
 
 use super::router::{SubscriptionId, SubscriptionIdRef};
-use crate::dialect::v0_34;
 use crate::{
     client::{
         subscription::SubscriptionTx,
@@ -33,6 +32,7 @@ use crate::{
         transport::router::{PublishResult, SubscriptionRouter},
         Client, CompatMode,
     },
+    dialect::{v0_34, Dialect, LatestDialect},
     endpoint::{self, subscribe, unsubscribe},
     error::Error,
     event::{self, Event},
@@ -220,11 +220,12 @@ impl WebSocketClient {
         }
     }
 
-    async fn perform_v0_34<R>(&self, request: R) -> Result<R::Output, Error>
+    async fn perform_with_dialect<R, S>(&self, request: R, dialect: S) -> Result<R::Output, Error>
     where
-        R: SimpleRequest<v0_34::Dialect>,
+        R: SimpleRequest<S>,
+        S: Dialect,
     {
-        self.inner.perform(request).await
+        self.inner.perform(request, dialect).await
     }
 }
 
@@ -234,7 +235,7 @@ impl Client for WebSocketClient {
     where
         R: SimpleRequest,
     {
-        self.inner.perform(request).await
+        self.perform_with_dialect(request, LatestDialect).await
     }
 
     async fn block_results<H>(&self, height: H) -> Result<endpoint::block_results::Response, Error>
@@ -259,7 +260,7 @@ impl Client for WebSocketClient {
                 // Back-fill with a request to /block endpoint and
                 // taking just the header from the response.
                 let resp = self
-                    .perform_v0_34(endpoint::block::Request::new(height))
+                    .perform_with_dialect(endpoint::block::Request::new(height), v0_34::Dialect)
                     .await?;
                 Ok(resp.into())
             },
@@ -279,7 +280,10 @@ impl Client for WebSocketClient {
                 // Back-fill with a request to /block_by_hash endpoint and
                 // taking just the header from the response.
                 let resp = self
-                    .perform_v0_34(endpoint::block_by_hash::Request::new(hash))
+                    .perform_with_dialect(
+                        endpoint::block_by_hash::Request::new(hash),
+                        v0_34::Dialect,
+                    )
                     .await?;
                 Ok(resp.into())
             },
@@ -618,7 +622,7 @@ mod sealed {
     }
 
     impl WebSocketClient {
-        pub async fn perform<R, S>(&self, request: R) -> Result<R::Output, Error>
+        pub async fn perform<R, S>(&self, request: R, _dialect: S) -> Result<R::Output, Error>
         where
             R: SimpleRequest<S>,
             S: Dialect,
@@ -650,8 +654,6 @@ mod sealed {
         fn into_client_request(
             self,
         ) -> tungstenite::Result<tungstenite::handshake::client::Request> {
-            let uri = self.to_string().parse::<http::Uri>().unwrap();
-
             let builder = tungstenite::handshake::client::Request::builder()
                 .method("GET")
                 .header("Host", self.host())
@@ -663,14 +665,14 @@ mod sealed {
                     tungstenite::handshake::client::generate_key(),
                 );
 
-            let builder = if let Some(auth) = authorize(&uri) {
+            let builder = if let Some(auth) = authorize(self.as_ref()) {
                 builder.header("Authorization", auth.to_string())
             } else {
                 builder
             };
 
             builder
-                .uri(uri)
+                .uri(self.to_string())
                 .body(())
                 .map_err(tungstenite::error::Error::HttpFormat)
         }
diff --git a/rpc/src/dialect.rs b/rpc/src/dialect.rs
index 176fe59b9..4d33a409e 100644
--- a/rpc/src/dialect.rs
+++ b/rpc/src/dialect.rs
@@ -3,6 +3,7 @@
 
 pub mod v0_34;
 pub mod v0_37;
+pub use v0_37::Dialect as LatestDialect;
 
 mod begin_block;
 mod check_tx;
@@ -23,8 +24,6 @@ pub trait Dialect: sealed::Sealed + Default + Clone + Send + Sync {
     type Evidence: From<evidence::Evidence> + Serialize + DeserializeOwned + Send;
 }
 
-pub type LatestDialect = v0_37::Dialect;
-
 mod sealed {
     pub trait Sealed {}
 
diff --git a/rpc/src/error.rs b/rpc/src/error.rs
index c1119708b..84f411c57 100644
--- a/rpc/src/error.rs
+++ b/rpc/src/error.rs
@@ -6,23 +6,11 @@ use flex_error::{define_error, DefaultTracer, DisplayError, DisplayOnly, ErrorMe
 
 use crate::{prelude::*, response_error::ResponseError, rpc_url::Url};
 
-#[cfg(feature = "http")]
-type HttpError = flex_error::DisplayOnly<http::Error>;
+#[cfg(feature = "reqwest")]
+type ReqwestError = flex_error::DisplayOnly<reqwest::Error>;
 
-#[cfg(not(feature = "http"))]
-type HttpError = flex_error::NoSource;
-
-#[cfg(feature = "http")]
-type InvalidUriError = flex_error::DisplayOnly<http::uri::InvalidUri>;
-
-#[cfg(not(feature = "http"))]
-type InvalidUriError = flex_error::NoSource;
-
-#[cfg(feature = "hyper")]
-type HyperError = flex_error::DisplayOnly<hyper::Error>;
-
-#[cfg(not(feature = "hyper"))]
-type HyperError = flex_error::NoSource;
+#[cfg(not(feature = "reqwest"))]
+type ReqwestError = flex_error::NoSource;
 
 #[cfg(feature = "tokio")]
 type JoinError = flex_error::DisplayOnly<tokio::task::JoinError>;
@@ -48,12 +36,12 @@ define_error! {
             | _ | { "I/O error" },
 
         Http
-            [ HttpError ]
+            [ ReqwestError ]
             | _ | { "HTTP error" },
 
-        Hyper
-            [ HyperError ]
-            | _ | { "HTTP error" },
+        InvalidProxy
+            [ ReqwestError ]
+            | _ | { "Invalid proxy configuration" },
 
         InvalidParams
             {
@@ -136,10 +124,6 @@ define_error! {
                 )
             },
 
-        InvalidUri
-            [ InvalidUriError ]
-            | _ | { "invalid URI" },
-
         Tendermint
             [ tendermint::Error ]
             | _ | { "tendermint error" },
diff --git a/rpc/src/rpc_url.rs b/rpc/src/rpc_url.rs
index 135f123d2..52ee3e0c5 100644
--- a/rpc/src/rpc_url.rs
+++ b/rpc/src/rpc_url.rs
@@ -49,33 +49,14 @@ impl FromStr for Scheme {
 pub struct Url {
     inner: url::Url,
     scheme: Scheme,
-    host: String,
-    port: u16,
 }
 
 impl FromStr for Url {
     type Err = Error;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let inner: url::Url = s.parse().map_err(Error::parse_url)?;
-
-        let scheme: Scheme = inner.scheme().parse()?;
-
-        let host = inner
-            .host_str()
-            .ok_or_else(|| Error::invalid_params(format!("URL is missing its host: {s}")))?
-            .to_owned();
-
-        let port = inner.port_or_known_default().ok_or_else(|| {
-            Error::invalid_params(format!("cannot determine appropriate port for URL: {s}"))
-        })?;
-
-        Ok(Self {
-            inner,
-            scheme,
-            host,
-            port,
-        })
+        let url: url::Url = s.parse().map_err(Error::parse_url)?;
+        url.try_into()
     }
 }
 
@@ -115,12 +96,12 @@ impl Url {
 
     /// Get the host associated with this URL.
     pub fn host(&self) -> &str {
-        &self.host
+        self.inner.host_str().unwrap()
     }
 
     /// Get the port associated with this URL.
     pub fn port(&self) -> u16 {
-        self.port
+        self.inner.port_or_known_default().unwrap()
     }
 
     /// Get this URL's path.
@@ -135,11 +116,37 @@ impl fmt::Display for Url {
     }
 }
 
+impl AsRef<url::Url> for Url {
+    fn as_ref(&self) -> &url::Url {
+        &self.inner
+    }
+}
+
+impl From<Url> for url::Url {
+    fn from(value: Url) -> Self {
+        value.inner
+    }
+}
+
 impl TryFrom<url::Url> for Url {
     type Error = crate::Error;
 
-    fn try_from(value: url::Url) -> Result<Self, Self::Error> {
-        value.to_string().parse()
+    fn try_from(url: url::Url) -> Result<Self, Self::Error> {
+        let scheme: Scheme = url.scheme().parse()?;
+
+        if url.host_str().is_none() {
+            return Err(Error::invalid_params(format!(
+                "URL is missing its host: {url}"
+            )));
+        }
+
+        if url.port_or_known_default().is_none() {
+            return Err(Error::invalid_params(format!(
+                "cannot determine appropriate port for URL: {url}"
+            )));
+        }
+
+        Ok(Self { inner: url, scheme })
     }
 }
 


Test patch: