use std::io::{self, Read};
use std::time::Duration;
use brazen::{Bytes, CanonicalError, ErrorKind, Method, Transport, TransportResponse, WireRequest};
pub struct HttpTransport {
agent: ureq::Agent,
}
impl HttpTransport {
pub fn new() -> Self {
let config = ureq::Agent::config_builder()
.http_status_as_error(false)
.build();
HttpTransport {
agent: config.into(),
}
}
}
impl Transport for HttpTransport {
fn send(&self, wire: WireRequest) -> Result<TransportResponse, CanonicalError> {
let t = wire.timeouts;
let resp = match wire.method {
Method::Get => {
let mut cfg = self.agent.get(&wire.url).config();
if let Some(secs) = t.connect {
cfg = cfg.timeout_connect(Some(Duration::from_secs(secs)));
}
if let Some(secs) = t.response {
cfg = cfg.timeout_recv_response(Some(Duration::from_secs(secs)));
}
let mut req = cfg.build();
for (name, value) in &wire.headers {
req = req.header(name, value);
}
req.call()
}
Method::Post => {
let mut cfg = self.agent.post(&wire.url).config();
if let Some(secs) = t.connect {
cfg = cfg.timeout_connect(Some(Duration::from_secs(secs)));
}
if let Some(secs) = t.response {
cfg = cfg.timeout_recv_response(Some(Duration::from_secs(secs)));
}
let mut req = cfg.build();
for (name, value) in &wire.headers {
req = req.header(name, value);
}
req.send(&wire.body[..])
}
}
.map_err(|e| transport_error(&e.to_string()))?;
let status = resp.status().as_u16();
let reader = resp.into_body().into_reader();
let body: Box<dyn Iterator<Item = io::Result<Bytes>>> = match t.idle {
Some(secs) => Box::new(IdleChunkReader::spawn(reader, Duration::from_secs(secs))),
None => Box::new(ChunkReader { reader }),
};
Ok(TransportResponse { status, body })
}
}
struct ChunkReader<R> {
reader: R,
}
impl<R: Read> Iterator for ChunkReader<R> {
type Item = io::Result<Bytes>;
fn next(&mut self) -> Option<Self::Item> {
let mut buf = vec![0u8; 8192];
match self.reader.read(&mut buf) {
Ok(0) => None,
Ok(n) => {
buf.truncate(n);
Some(Ok(buf))
}
Err(e) => Some(Err(e)),
}
}
}
struct IdleChunkReader {
rx: std::sync::mpsc::Receiver<io::Result<Bytes>>,
idle: Duration,
done: bool,
}
impl IdleChunkReader {
fn spawn<R: Read + Send + 'static>(reader: R, idle: Duration) -> Self {
let (tx, rx) = std::sync::mpsc::sync_channel::<io::Result<Bytes>>(0);
std::thread::spawn(move || {
let mut reader = reader;
loop {
let mut buf = vec![0u8; 8192];
let item = match reader.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
buf.truncate(n);
Ok(buf)
}
Err(e) => Err(e),
};
let is_err = item.is_err();
if tx.send(item).is_err() || is_err {
break;
}
}
});
IdleChunkReader {
rx,
idle,
done: false,
}
}
}
impl Iterator for IdleChunkReader {
type Item = io::Result<Bytes>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
match self.rx.recv_timeout(self.idle) {
Ok(item) => {
self.done = item.is_err();
Some(item)
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
self.done = true;
Some(Err(io::Error::new(
io::ErrorKind::TimedOut,
"stream stalled: no data within the idle-read timeout",
)))
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
self.done = true;
None
}
}
}
}
fn transport_error(message: &str) -> CanonicalError {
CanonicalError {
kind: ErrorKind::Transport,
message: format!("HTTP transport: {message}"),
provider_detail: None,
}
}
#[cfg(test)]
mod tests;