use crate::errors::{RemoteError, TimeoutError};
use async_compression::stream::{GzipDecoder, ZlibDecoder};
use bytes::{Buf, Bytes};
use conjure_error::Error;
use futures::task::Context;
use futures::{ready, FutureExt, Stream, StreamExt, TryStreamExt};
use hyper::http::header::CONTENT_ENCODING;
use hyper::{HeaderMap, StatusCode};
use std::io;
use std::mem;
use std::mem::MaybeUninit;
use std::pin::Pin;
use std::task::Poll;
use std::time::Instant;
use tokio::io::{AsyncBufRead, AsyncRead, AsyncReadExt};
use tokio::time::{self, Delay};
use witchcraft_log::info;
use zipkin::{Detached, OpenSpan};
pub struct Response {
status: StatusCode,
headers: HeaderMap,
body: ResponseBody,
}
impl Response {
pub(crate) fn new(
response: hyper::Response<hyper::Body>,
deadline: Instant,
span: OpenSpan<Detached>,
) -> Result<Response, Error> {
let (parts, body) = response.into_parts();
let body = ResponseBody::new(&parts.headers, body, deadline, span)?;
Ok(Response {
status: parts.status,
headers: parts.headers,
body,
})
}
pub(crate) async fn into_error(self, propagate_service_errors: bool) -> Error {
let status = self.status();
let mut buf = vec![];
if let Err(e) = self.into_body().take(10 * 1024).read_to_end(&mut buf).await {
info!(
"error reading response body",
error: Error::internal_safe(e),
);
}
let error = RemoteError {
status,
error: conjure_serde::json::client_from_slice(&buf).ok(),
};
let log_body = error.error.is_none();
let mut error = match &error.error {
Some(e) if propagate_service_errors => {
let e = e.clone();
Error::service_safe(error, e)
}
_ => Error::internal_safe(error),
};
if log_body {
error = error.with_unsafe_param("body", String::from_utf8_lossy(&buf));
}
error
}
pub fn status(&self) -> StatusCode {
self.status
}
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
pub fn into_body(self) -> ResponseBody {
self.body
}
}
pub struct ResponseBody {
stream: Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Sync + Send>>,
cur: Bytes,
}
impl ResponseBody {
#[allow(clippy::borrow_interior_mutable_const)]
fn new(
headers: &HeaderMap,
body: hyper::Body,
deadline: Instant,
span: OpenSpan<Detached>,
) -> Result<ResponseBody, Error> {
let body = IdentityBody {
body,
deadline: time::delay_until(tokio::time::Instant::from_std(deadline)),
_span: span,
};
let stream: Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Sync + Send>> =
match headers.get(&CONTENT_ENCODING) {
Some(v) if v == "gzip" => Box::pin(GzipDecoder::new(body)),
Some(v) if v == "deflate" => Box::pin(ZlibDecoder::new(body)),
Some(v) if v == "identity" => Box::pin(body),
None => Box::pin(body),
Some(v) => {
return Err(Error::internal_safe("unsupported Content-Encoding")
.with_safe_param("encoding", format!("{:?}", v)));
}
};
Ok(ResponseBody {
stream,
cur: Bytes::new(),
})
}
pub async fn read_bytes(&mut self) -> io::Result<Option<Bytes>> {
if self.cur.has_remaining() {
Ok(Some(mem::replace(&mut self.cur, Bytes::new())))
} else {
self.stream.try_next().await
}
}
}
impl AsyncRead for ResponseBody {
unsafe fn prepare_uninitialized_buffer(&self, _: &mut [MaybeUninit<u8>]) -> bool {
false
}
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
let read_buf = ready!(self.as_mut().poll_fill_buf(cx))?;
let nread = usize::min(buf.len(), read_buf.len());
buf[..nread].copy_from_slice(&read_buf[..nread]);
self.consume(nread);
Poll::Ready(Ok(nread))
}
}
impl AsyncBufRead for ResponseBody {
fn poll_fill_buf(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<&[u8]>> {
while !self.cur.has_remaining() {
match ready!(self.stream.poll_next_unpin(cx)).transpose()? {
Some(bytes) => self.cur = bytes,
None => break,
}
}
Poll::Ready(Ok(&self.get_mut().cur))
}
fn consume(mut self: Pin<&mut Self>, amt: usize) {
self.cur.advance(amt);
}
}
struct IdentityBody {
body: hyper::Body,
deadline: Delay,
_span: OpenSpan<Detached>,
}
impl Stream for IdentityBody {
type Item = io::Result<Bytes>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if let Poll::Ready(()) = self.deadline.poll_unpin(cx) {
return Poll::Ready(Some(Err(io::Error::new(
io::ErrorKind::TimedOut,
TimeoutError(()),
))));
}
match self.body.poll_next_unpin(cx) {
Poll::Ready(Some(Ok(chunk))) => Poll::Ready(Some(Ok(chunk))),
Poll::Ready(Some(Err(e))) => {
Poll::Ready(Some(Err(io::Error::new(io::ErrorKind::Other, e))))
}
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.body.size_hint()
}
}