use std::fmt;
use std::sync::{
Arc,
Mutex,
};
use std::time::Duration;
use async_stream::stream;
use bytes::Bytes;
use futures_util::{
stream as futures_stream,
StreamExt,
};
use http::header::{
CONTENT_LENGTH,
CONTENT_TYPE,
};
use http::{
HeaderMap,
Method,
StatusCode,
};
use serde::de::DeserializeOwned;
use tokio_util::sync::CancellationToken;
use url::Url;
use crate::content_type;
use crate::error::{
backend_error_mapper::map_reqwest_error,
ReqwestErrorPhase,
};
use crate::sanitize::{
BodyLogContext,
BodyPreview,
LogSanitizer,
SanitizedDebugger,
};
use crate::sse::{
DoneMarkerPolicy,
SseChunkStream,
SseJsonMode,
SseMessageStream,
};
use crate::{
HttpByteStream,
HttpError,
HttpErrorKind,
HttpResult,
};
use super::{
HttpResponseMeta,
HttpResponseOptions,
};
#[derive(Debug, Clone)]
struct BodyReadFailure {
kind: HttpErrorKind,
message: String,
method: Option<Method>,
url: Option<Url>,
status: Option<StatusCode>,
}
impl BodyReadFailure {
fn from_error(error: &HttpError) -> Self {
Self {
kind: error.kind,
message: error.message.clone(),
method: error.method.clone(),
url: error.url.clone(),
status: error.status,
}
}
fn to_error(&self) -> HttpError {
let mut error = HttpError::new(
self.kind,
format!("previous response body read failed: {}", self.message),
);
if let Some(method) = self.method.as_ref() {
error = error.with_method(method);
}
if let Some(url) = self.url.as_ref() {
error = error.with_url(url);
}
if let Some(status) = self.status {
error = error.with_status(status);
}
error
}
}
type BodyReadFailureState = Arc<Mutex<Option<BodyReadFailure>>>;
fn map_response_read_error(error: reqwest::Error, method: Method, url: Url, status: StatusCode) -> HttpError {
if error.is_timeout() {
return map_reqwest_error(error, HttpErrorKind::Transport, ReqwestErrorPhase::Read, method, url)
.with_status(status);
}
let error = error.without_url();
HttpError::transport(format!("Failed to read response body: {}", error))
.with_method(&method)
.with_url(&url)
.with_status(status)
.with_source(error)
}
#[derive(Debug, Clone)]
struct HttpResponseRuntime {
read_timeout: Duration,
cancellation_token: Option<CancellationToken>,
request_url: Url,
body_read_failure: BodyReadFailureState,
}
impl HttpResponseRuntime {
fn new(read_timeout: Duration, cancellation_token: Option<CancellationToken>, request_url: Url) -> Self {
Self {
read_timeout,
cancellation_token,
request_url,
body_read_failure: Arc::new(Mutex::new(None)),
}
}
}
pub struct HttpResponse {
pub(crate) meta: HttpResponseMeta,
backend: Option<reqwest::Response>,
buffered_body: Option<Bytes>,
runtime: HttpResponseRuntime,
options: HttpResponseOptions,
}
impl fmt::Debug for HttpResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let debugger = SanitizedDebugger::new(self.options.log_sanitizer.policy());
let url = debugger.url(self.meta.url());
let request_url = debugger.url(&self.runtime.request_url);
formatter
.debug_struct("HttpResponse")
.field("status", &self.meta.status())
.field("headers", &debugger.headers(self.meta.headers()))
.field("url", &url)
.field("request_url", &request_url)
.field("method", self.meta.method())
.field("backend_present", &self.backend.is_some())
.field("buffered_body_len", &self.buffered_body.as_ref().map(Bytes::len))
.field("read_timeout", &self.runtime.read_timeout)
.field("cancellation_token_present", &self.runtime.cancellation_token.is_some())
.field("options", &self.options)
.finish()
}
}
impl HttpResponse {
pub fn new(status: StatusCode, headers: HeaderMap, body: Bytes, url: Url, method: Method) -> Self {
Self {
meta: HttpResponseMeta::new(status, headers, url.clone(), method),
backend: None,
buffered_body: Some(body),
runtime: HttpResponseRuntime::new(Duration::from_secs(30), None, url),
options: HttpResponseOptions::default(),
}
}
pub(crate) fn from_backend(
meta: HttpResponseMeta,
backend: reqwest::Response,
read_timeout: Duration,
cancellation_token: Option<CancellationToken>,
request_url: Url,
options: HttpResponseOptions,
) -> Self {
Self {
meta,
backend: Some(backend),
buffered_body: None,
runtime: HttpResponseRuntime::new(read_timeout, cancellation_token, request_url),
options,
}
}
#[inline]
pub fn meta(&self) -> &HttpResponseMeta {
&self.meta
}
#[inline]
pub fn status(&self) -> StatusCode {
self.meta.status()
}
#[inline]
pub fn headers(&self) -> &HeaderMap {
self.meta.headers()
}
#[inline]
pub fn url(&self) -> &Url {
self.meta.url()
}
#[inline]
pub fn request_url(&self) -> &Url {
&self.runtime.request_url
}
#[inline]
pub fn is_success(&self) -> bool {
self.status().is_success()
}
#[inline]
pub fn retry_after_hint(&self) -> Option<Duration> {
self.meta.retry_after_hint()
}
fn previous_body_read_error(&self) -> Option<HttpError> {
let guard = self.runtime.body_read_failure.lock().ok()?;
guard.as_ref().map(BodyReadFailure::to_error)
}
fn remember_body_read_failure(&self, error: &HttpError) {
Self::remember_body_read_failure_state(&self.runtime.body_read_failure, error);
}
fn remember_body_read_failure_state(state: &BodyReadFailureState, error: &HttpError) {
if let Ok(mut guard) = state.lock() {
guard.get_or_insert_with(|| BodyReadFailure::from_error(error));
}
}
pub(crate) async fn into_success_or_status_error(self, message_prefix: &str) -> HttpResult<Self> {
let status = self.status();
if status.is_success() {
return Ok(self);
}
let retry_after = self.retry_after_hint();
let method = self.meta.method().clone();
let url = self.request_url().clone();
let error_preview_limit = self.options.error_response_preview_limit;
let diagnostic_sanitizer = LogSanitizer::for_debug(self.options.log_sanitizer.policy());
let body_preview = self.into_error_body_preview(error_preview_limit).await?;
let log_sanitize_policy = diagnostic_sanitizer.policy().clone();
let message = format!(
"{} with status {} for {} {}; response body preview: {}",
message_prefix,
status,
method,
diagnostic_sanitizer.sanitize_url(&url),
body_preview
);
let mut mapped = HttpError::status(status, message)
.with_method(&method)
.with_url(&url)
.with_response_body_preview(body_preview)
.with_log_sanitize_policy(log_sanitize_policy);
if let Some(retry_after) = retry_after {
mapped = mapped.with_retry_after(retry_after);
}
Err(mapped)
}
pub(crate) async fn into_error_body_preview(mut self, max_bytes: usize) -> HttpResult<String> {
let limit = max_bytes.max(1);
let Some(backend) = self.backend.take() else {
return Ok("<empty>".to_string());
};
let content_type = Self::content_type_value(self.meta.headers());
self.read_error_body_preview(backend, limit, content_type).await
}
pub async fn bytes(&mut self) -> HttpResult<Bytes> {
if let Some(body) = &self.buffered_body {
return Ok(body.clone());
}
if let Some(error) = self.previous_body_read_error() {
return Err(error);
}
let Some(mut backend) = self.backend.take() else {
self.buffered_body = Some(Bytes::new());
return Ok(Bytes::new());
};
let method = self.meta.method().clone();
let url = self.runtime.request_url.clone();
let status = self.meta.status();
let read_timeout = self.runtime.read_timeout;
let cancellation_token = self.runtime.cancellation_token.clone();
let mut body = bytes::BytesMut::new();
loop {
let next = if let Some(token) = &cancellation_token {
tokio::select! {
_ = token.cancelled() => {
let error = HttpError::cancelled("Request cancelled while reading response body")
.with_method(&method)
.with_url(&url)
.with_status(status);
self.remember_body_read_failure(&error);
return Err(error);
}
item = tokio::time::timeout(read_timeout, backend.chunk()) => item,
}
} else {
tokio::time::timeout(read_timeout, backend.chunk()).await
};
match next {
Ok(Ok(Some(chunk))) => body.extend_from_slice(&chunk),
Ok(Ok(None)) => {
let body = body.freeze();
self.buffered_body = Some(body.clone());
return Ok(body);
}
Ok(Err(error)) => {
let error = map_response_read_error(error, method, url, status);
self.remember_body_read_failure(&error);
return Err(error);
}
Err(_) => {
let error = HttpError::read_timeout(format!(
"Read timeout after {:?} while reading response body",
read_timeout
))
.with_method(self.meta.method())
.with_url(&self.runtime.request_url)
.with_status(status);
self.remember_body_read_failure(&error);
return Err(error);
}
}
}
}
pub fn stream(&mut self) -> HttpResult<HttpByteStream> {
if let Some(body) = self.buffered_body.as_ref() {
let bytes = body.clone();
return Ok(Box::pin(futures_stream::once(async move { Ok(bytes) })));
}
if let Some(error) = self.previous_body_read_error() {
return Err(error);
}
if let Some(error) = self.cancelled_error_if_needed("Streaming response cancelled before reading response body")
{
return Err(error);
}
let Some(backend) = self.backend.take() else {
return Ok(Box::pin(futures_stream::empty()));
};
let method = self.meta.method().clone();
let url = self.runtime.request_url.clone();
let status = self.meta.status();
let read_timeout = self.runtime.read_timeout;
let cancellation_token = self.runtime.cancellation_token.clone();
let body_read_failure = self.runtime.body_read_failure.clone();
let mut stream = backend.bytes_stream();
let wrapped = stream! {
loop {
let next = if let Some(token) = &cancellation_token {
tokio::select! {
_ = token.cancelled() => {
let error = HttpError::cancelled("Streaming response cancelled while reading body")
.with_method(&method)
.with_url(&url)
.with_status(status);
Self::remember_body_read_failure_state(&body_read_failure, &error);
yield Err(error);
break;
}
item = tokio::time::timeout(read_timeout, stream.next()) => item,
}
} else {
tokio::time::timeout(read_timeout, stream.next()).await
};
match next {
Ok(Some(Ok(bytes))) => yield Ok(bytes),
Ok(Some(Err(error))) => {
let mapped = map_response_read_error(error, method.clone(), url.clone(), status);
Self::remember_body_read_failure_state(&body_read_failure, &mapped);
yield Err(mapped);
break;
}
Ok(None) => break,
Err(_) => {
let error = HttpError::read_timeout(format!(
"Read timeout after {:?} while streaming response",
read_timeout
))
.with_method(&method)
.with_url(&url)
.with_status(status);
Self::remember_body_read_failure_state(&body_read_failure, &error);
yield Err(error);
break;
}
}
}
};
Ok(Box::pin(wrapped))
}
pub async fn text(&mut self) -> HttpResult<String> {
let body = self.bytes().await?;
String::from_utf8(body.to_vec()).map_err(|error| {
HttpError::decode(format!("Failed to decode response body as UTF-8: {}", error))
.with_status(self.meta.status())
.with_url(self.meta.url())
})
}
pub async fn json<T>(&mut self) -> HttpResult<T>
where
T: DeserializeOwned,
{
let body = self.bytes().await?;
serde_json::from_slice(&body).map_err(|error| {
HttpError::decode(format!("Failed to decode response JSON: {}", error))
.with_status(self.meta.status())
.with_url(self.meta.url())
})
}
#[inline]
pub fn sse_max_line_bytes(mut self, max_line_bytes: usize) -> Self {
self.options.sse_max_line_bytes = max_line_bytes.max(1);
self
}
#[inline]
pub fn sse_max_frame_bytes(mut self, max_frame_bytes: usize) -> Self {
self.options.sse_max_frame_bytes = max_frame_bytes.max(1);
self
}
#[inline]
pub fn sse_json_mode(mut self, mode: SseJsonMode) -> Self {
self.options.sse_json_mode = mode;
self
}
#[inline]
pub fn sse_done_marker_policy(mut self, policy: DoneMarkerPolicy) -> Self {
self.options.sse_done_marker_policy = policy;
self
}
pub fn sse_messages(mut self) -> SseMessageStream {
let max_line_bytes = self.options.sse_max_line_bytes;
let max_frame_bytes = self.options.sse_max_frame_bytes;
match self.stream() {
Ok(stream) => crate::sse::decode_messages_from_stream_with_limits(stream, max_line_bytes, max_frame_bytes),
Err(error) => Box::pin(futures_stream::once(async move { Err(error) })),
}
}
pub(crate) fn sse_records(mut self) -> crate::sse::SseRecordStream {
let max_line_bytes = self.options.sse_max_line_bytes;
let max_frame_bytes = self.options.sse_max_frame_bytes;
match self.stream() {
Ok(stream) => crate::sse::decode_records_from_stream_with_limits(stream, max_line_bytes, max_frame_bytes),
Err(error) => Box::pin(futures_stream::once(async move { Err(error) })),
}
}
pub fn sse_chunks<T>(mut self) -> SseChunkStream<T>
where
T: DeserializeOwned + Send + 'static,
{
let done_policy = self.options.sse_done_marker_policy.clone();
let mode = self.options.sse_json_mode;
let max_line_bytes = self.options.sse_max_line_bytes;
let max_frame_bytes = self.options.sse_max_frame_bytes;
match self.stream() {
Ok(stream) => crate::sse::decode_json_chunks_from_stream_with_limits(
stream,
done_policy,
mode,
max_line_bytes,
max_frame_bytes,
),
Err(error) => Box::pin(futures_stream::once(async move { Err(error) })),
}
}
pub(crate) fn buffered_body_for_logging(&self) -> Option<&Bytes> {
self.buffered_body.as_ref()
}
pub(crate) fn can_buffer_body_for_logging(&self, body_log_limit: usize) -> bool {
if self.backend.is_none() {
return false;
}
if self.is_sse_response() {
return false;
}
self.content_length_hint()
.is_some_and(|content_length| content_length <= body_log_limit as u64)
}
async fn read_error_body_preview(
&self,
mut response: reqwest::Response,
max_bytes: usize,
content_type: Option<String>,
) -> HttpResult<String> {
let limit = max_bytes.max(1);
let read_timeout = self.runtime.read_timeout;
let cancellation_token = self.runtime.cancellation_token.clone();
let method = self.meta.method().clone();
let url = self.runtime.request_url.clone();
let status = self.meta.status();
let mut preview = Vec::new();
let mut truncated = false;
let capture_limit = limit.saturating_add(1);
loop {
let next = if let Some(token) = cancellation_token.as_ref() {
tokio::select! {
_ = token.cancelled() => {
return Err(HttpError::cancelled(
"Request cancelled while reading status error response body preview",
)
.with_method(&method)
.with_url(&url)
.with_status(status));
}
item = tokio::time::timeout(read_timeout, response.chunk()) => item,
}
} else {
tokio::time::timeout(read_timeout, response.chunk()).await
};
match next {
Ok(Ok(Some(chunk))) => {
let remaining = capture_limit.saturating_sub(preview.len());
if chunk.len() > remaining {
preview.extend_from_slice(&chunk[..remaining]);
truncated = true;
break;
}
preview.extend_from_slice(&chunk);
if preview.len() > limit {
truncated = true;
break;
}
}
Ok(Ok(None)) => break,
Ok(Err(error)) => {
let error = error.without_url();
return Ok(format!(
"<error body unavailable: failed to read response body: {}>",
error
));
}
Err(_) => {
return Ok(format!(
"<error body unavailable: read timeout after {:?}>",
read_timeout
));
}
}
}
let source_len = preview.len();
if preview.len() > limit {
preview.truncate(limit);
truncated = true;
}
Ok(Self::render_error_body_preview(
&preview,
source_len,
truncated,
content_type.as_deref(),
&self.options.log_sanitizer,
))
}
fn cancelled_error_if_needed(&self, message: &str) -> Option<HttpError> {
if self
.runtime
.cancellation_token
.as_ref()
.is_some_and(CancellationToken::is_cancelled)
{
Some(
HttpError::cancelled(message.to_string())
.with_method(self.meta.method())
.with_url(&self.runtime.request_url),
)
} else {
None
}
}
fn content_length_hint(&self) -> Option<u64> {
self.meta
.headers()
.get(CONTENT_LENGTH)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<u64>().ok())
}
fn is_sse_response(&self) -> bool {
self.meta
.headers()
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.is_some_and(content_type::is_sse)
}
fn render_error_body_preview(
bytes: &[u8],
source_len: usize,
truncated: bool,
content_type: Option<&str>,
log_sanitizer: &LogSanitizer,
) -> String {
let preview = BodyPreview::from_limited_bytes(bytes, source_len, truncated, BodyLogContext::ErrorResponse);
let preview = if let Some(content_type) = content_type {
preview.with_content_type(content_type)
} else {
preview
};
log_sanitizer.sanitize_body_preview(&preview)
}
fn content_type_value(headers: &HeaderMap) -> Option<String> {
headers
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.map(str::to_string)
}
}