forked from kent/consciousness
simplify http library
This commit is contained in:
parent
6c4a88d2ab
commit
78912ca72f
1 changed files with 40 additions and 55 deletions
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use http_body_util::{BodyExt, Full, Empty};
|
use http_body_util::{BodyExt, Full};
|
||||||
use hyper::body::Incoming;
|
use hyper::body::Incoming;
|
||||||
use hyper::{Request, StatusCode};
|
use hyper::{Request, StatusCode};
|
||||||
use hyper_util::rt::TokioIo;
|
use hyper_util::rt::TokioIo;
|
||||||
|
|
@ -47,27 +47,19 @@ impl HttpClient {
|
||||||
|
|
||||||
/// Send a GET request with custom headers.
|
/// Send a GET request with custom headers.
|
||||||
pub async fn get_with_headers(&self, url: &str, headers: &[(&str, &str)]) -> Result<HttpResponse> {
|
pub async fn get_with_headers(&self, url: &str, headers: &[(&str, &str)]) -> Result<HttpResponse> {
|
||||||
let mut builder = Request::get(url);
|
self.send(url, "GET", headers, Bytes::new()).await
|
||||||
for &(k, v) in headers {
|
|
||||||
builder = builder.header(k, v);
|
|
||||||
}
|
|
||||||
let req = builder.body(Empty::<Bytes>::new())
|
|
||||||
.context("building GET request")?;
|
|
||||||
self.send_empty(req).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Send a POST request with URL-encoded form data.
|
/// Send a POST request with URL-encoded form data.
|
||||||
pub async fn post_form(&self, url: &str, params: &[(&str, &str)]) -> Result<HttpResponse> {
|
pub async fn post_form(&self, url: &str, params: &[(&str, &str)]) -> Result<HttpResponse> {
|
||||||
let body = serde_urlencoded::to_string(params).context("encoding form")?;
|
let body = serde_urlencoded::to_string(params).context("encoding form")?;
|
||||||
let req = Request::post(url)
|
self.send(url, "POST",
|
||||||
.header("content-type", "application/x-www-form-urlencoded")
|
&[("content-type", "application/x-www-form-urlencoded")],
|
||||||
.body(Full::new(Bytes::from(body)))
|
Bytes::from(body),
|
||||||
.context("building form POST")?;
|
).await
|
||||||
self.send_full(req).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a request with headers pre-set. JSON body.
|
/// Send a request with JSON body.
|
||||||
pub async fn send_json(
|
pub async fn send_json(
|
||||||
&self,
|
&self,
|
||||||
method: &str,
|
method: &str,
|
||||||
|
|
@ -76,66 +68,59 @@ impl HttpClient {
|
||||||
body: &impl serde::Serialize,
|
body: &impl serde::Serialize,
|
||||||
) -> Result<HttpResponse> {
|
) -> Result<HttpResponse> {
|
||||||
let json = serde_json::to_vec(body).context("serializing JSON body")?;
|
let json = serde_json::to_vec(body).context("serializing JSON body")?;
|
||||||
let mut builder = Request::builder()
|
let mut all_headers = vec![("content-type", "application/json")];
|
||||||
.method(method)
|
all_headers.extend_from_slice(headers);
|
||||||
.uri(url)
|
self.send(url, method, &all_headers, Bytes::from(json)).await
|
||||||
.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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn connect(&self, url: &str) -> Result<(bool, TokioIo<Box<dyn IoStream>>)> {
|
/// 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<HttpResponse> {
|
||||||
let uri: http::Uri = url.parse().context("parsing URL")?;
|
let uri: http::Uri = url.parse().context("parsing URL")?;
|
||||||
let host = uri.host().context("URL has no host")?.to_string();
|
let host = uri.host().context("URL has no host")?.to_string();
|
||||||
let is_https = uri.scheme_str() == Some("https");
|
let is_https = uri.scheme_str() == Some("https");
|
||||||
let port = uri.port_u16().unwrap_or(if is_https { 443 } else { 80 });
|
let port = uri.port_u16().unwrap_or(if is_https { 443 } else { 80 });
|
||||||
|
|
||||||
|
// Connect
|
||||||
let tcp = tokio::time::timeout(
|
let tcp = tokio::time::timeout(
|
||||||
self.connect_timeout,
|
self.connect_timeout,
|
||||||
TcpStream::connect(format!("{}:{}", host, port)),
|
TcpStream::connect(format!("{host}:{port}")),
|
||||||
).await
|
).await
|
||||||
.context("connect timeout")?
|
.context("connect timeout")?
|
||||||
.context("TCP connect")?;
|
.context("TCP connect")?;
|
||||||
|
|
||||||
if is_https {
|
let io: TokioIo<Box<dyn IoStream>> = if is_https {
|
||||||
let server_name = rustls::pki_types::ServerName::try_from(host.clone())
|
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 connector = tokio_rustls::TlsConnector::from(self.tls.clone());
|
||||||
let tls = connector.connect(server_name.to_owned(), tcp).await
|
let tls = connector.connect(server_name.to_owned(), tcp).await
|
||||||
.context("TLS handshake")?;
|
.context("TLS handshake")?;
|
||||||
Ok((is_https, TokioIo::new(Box::new(tls) as Box<dyn IoStream>)))
|
TokioIo::new(Box::new(tls) as Box<dyn IoStream>)
|
||||||
} else {
|
} else {
|
||||||
Ok((is_https, TokioIo::new(Box::new(tcp) as Box<dyn IoStream>)))
|
TokioIo::new(Box::new(tcp) as Box<dyn IoStream>)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
let req = builder.body(Full::new(body))
|
||||||
|
.context("building request")?;
|
||||||
async fn send_full(&self, req: Request<Full<Bytes>>) -> Result<HttpResponse> {
|
|
||||||
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<Empty<Bytes>>) -> Result<HttpResponse> {
|
|
||||||
let url = req.uri().to_string();
|
|
||||||
let (_is_https, io) = self.connect(&url).await?;
|
|
||||||
|
|
||||||
|
// Send
|
||||||
let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await
|
let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await
|
||||||
.context("HTTP handshake")?;
|
.context("HTTP handshake")?;
|
||||||
tokio::spawn(conn);
|
tokio::spawn(conn);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue