diff --git a/src/agent/api/http.rs b/src/agent/api/http.rs index 6220792..429350b 100644 --- a/src/agent/api/http.rs +++ b/src/agent/api/http.rs @@ -5,7 +5,7 @@ use anyhow::{Context, Result}; use bytes::Bytes; -use http_body_util::{BodyExt, Full, Empty}; +use http_body_util::{BodyExt, Full}; use hyper::body::Incoming; use hyper::{Request, StatusCode}; use hyper_util::rt::TokioIo; @@ -47,27 +47,19 @@ impl HttpClient { /// Send a GET request with custom headers. pub async fn get_with_headers(&self, url: &str, headers: &[(&str, &str)]) -> Result { - let mut builder = Request::get(url); - for &(k, v) in headers { - builder = builder.header(k, v); - } - let req = builder.body(Empty::::new()) - .context("building GET request")?; - self.send_empty(req).await + self.send(url, "GET", headers, Bytes::new()).await } - /// Send a POST request with URL-encoded form data. pub async fn post_form(&self, url: &str, params: &[(&str, &str)]) -> Result { let body = serde_urlencoded::to_string(params).context("encoding form")?; - let req = Request::post(url) - .header("content-type", "application/x-www-form-urlencoded") - .body(Full::new(Bytes::from(body))) - .context("building form POST")?; - self.send_full(req).await + self.send(url, "POST", + &[("content-type", "application/x-www-form-urlencoded")], + Bytes::from(body), + ).await } - /// Send a request with headers pre-set. JSON body. + /// Send a request with JSON body. pub async fn send_json( &self, method: &str, @@ -76,66 +68,59 @@ impl HttpClient { body: &impl serde::Serialize, ) -> Result { let json = serde_json::to_vec(body).context("serializing JSON body")?; - let mut builder = Request::builder() - .method(method) - .uri(url) - .header("content-type", "application/json"); - for &(k, v) in headers { - builder = builder.header(k, v); - } - let req = builder.body(Full::new(Bytes::from(json))) - .context("building request")?; - self.send_full(req).await + let mut all_headers = vec![("content-type", "application/json")]; + all_headers.extend_from_slice(headers); + self.send(url, method, &all_headers, Bytes::from(json)).await } - async fn connect(&self, url: &str) -> Result<(bool, TokioIo>)> { + /// Core send: parse URL, connect, build request with correct + /// path-only URI and Host header, send, return response. + async fn send( + &self, + url: &str, + method: &str, + headers: &[(&str, &str)], + body: Bytes, + ) -> Result { let uri: http::Uri = url.parse().context("parsing URL")?; let host = uri.host().context("URL has no host")?.to_string(); let is_https = uri.scheme_str() == Some("https"); let port = uri.port_u16().unwrap_or(if is_https { 443 } else { 80 }); + // Connect let tcp = tokio::time::timeout( self.connect_timeout, - TcpStream::connect(format!("{}:{}", host, port)), + TcpStream::connect(format!("{host}:{port}")), ).await .context("connect timeout")? .context("TCP connect")?; - if is_https { + let io: TokioIo> = if is_https { let server_name = rustls::pki_types::ServerName::try_from(host.clone()) - .map_err(|e| anyhow::anyhow!("invalid server name: {}", e))?; + .map_err(|e| anyhow::anyhow!("invalid server name: {e}"))?; let connector = tokio_rustls::TlsConnector::from(self.tls.clone()); let tls = connector.connect(server_name.to_owned(), tcp).await .context("TLS handshake")?; - Ok((is_https, TokioIo::new(Box::new(tls) as Box))) + TokioIo::new(Box::new(tls) as Box) } else { - Ok((is_https, TokioIo::new(Box::new(tcp) as Box))) + TokioIo::new(Box::new(tcp) as Box) + }; + + // Build request with path-only URI and Host header + let path_and_query = uri.path_and_query() + .map(|pq| pq.as_str()) + .unwrap_or("/"); + let mut builder = Request::builder() + .method(method) + .uri(path_and_query) + .header("host", &host); + for &(k, v) in headers { + builder = builder.header(k, v); } - } - - async fn send_full(&self, req: Request>) -> Result { - let url = req.uri().to_string(); - let (_is_https, io) = self.connect(&url).await?; - - let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await - .context("HTTP handshake")?; - tokio::spawn(conn); - - let resp = tokio::time::timeout( - self.request_timeout, - sender.send_request(req), - ).await - .context("request timeout")? - .context("sending request")?; - - let (parts, body) = resp.into_parts(); - Ok(HttpResponse { parts, body }) - } - - async fn send_empty(&self, req: Request>) -> Result { - let url = req.uri().to_string(); - let (_is_https, io) = self.connect(&url).await?; + let req = builder.body(Full::new(body)) + .context("building request")?; + // Send let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await .context("HTTP handshake")?; tokio::spawn(conn);