#[cfg(test)]
#[path = "tests/http.rs"]
mod tests;
use base64::{DecodeError, engine::{Engine as _, general_purpose::STANDARD as BASE64}};
use bytes::Bytes;
use core::{
cmp::Ordering,
convert::Infallible,
error::Error,
fmt::{Debug, Display, Write, self},
ops::{Add, AddAssign},
str::FromStr,
};
use futures::executor;
use futures_util::FutureExt as _;
use http::{Response, StatusCode};
use http_body_util::{BodyExt as _, Collected, Full};
use hyper::{
body::Incoming,
HeaderMap,
header::HeaderValue,
};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError};
use serde_json::Value as Json;
use std::borrow::Cow;
use thiserror::Error as ThisError;
#[cfg(feature = "axum")]
use ::{
axum::body::{Body as AxumBody, to_bytes},
core::mem,
};
#[expect(clippy::exhaustive_enums, reason = "Exhaustive")]
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum ContentType {
#[default]
Text,
Binary,
}
#[derive(Debug, ThisError)]
#[non_exhaustive]
pub enum ResponseError {
#[error("Error encountered while converting response body to bytes: {0}")]
ConversionError(Box<dyn Error>),
}
#[derive(Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct UnpackedResponse {
#[serde(serialize_with = "serialize_status_code", deserialize_with = "deserialize_status_code")]
pub status: StatusCode,
pub headers: Vec<UnpackedResponseHeader>,
pub body: UnpackedResponseBody,
}
impl UnpackedResponse {
#[must_use]
pub fn new<T: Into<UnpackedResponseBody>>(
status: StatusCode,
headers: Vec<(String, String)>,
body: T
) -> Self {
Self::new_from_parts(
status,
headers.into_iter().map(|(name, value)| UnpackedResponseHeader::new(name, value)).collect(),
body.into(),
)
}
#[must_use]
pub const fn new_from_parts(
status: StatusCode,
headers: Vec<UnpackedResponseHeader>,
body: UnpackedResponseBody
) -> Self {
Self {
status,
headers,
body,
}
}
}
impl PartialEq for UnpackedResponse {
fn eq(&self, other: &Self) -> bool {
self.status == other.status && self.headers == other.headers && self.body == other.body
}
}
#[expect(clippy::exhaustive_structs, reason = "Exhaustive")]
#[derive(Debug, Deserialize, Serialize)]
pub struct UnpackedResponseHeader {
pub name: String,
pub value: String,
}
impl UnpackedResponseHeader {
#[must_use]
pub const fn new(name: String, value: String) -> Self {
Self {
name,
value,
}
}
}
impl PartialEq for UnpackedResponseHeader {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.value == other.value
}
}
#[derive(Default)]
#[non_exhaustive]
pub struct UnpackedResponseBody {
body: Vec<u8>,
content_type: ContentType,
}
impl UnpackedResponseBody {
pub fn new<T: Into<Self>>(data: T) -> Self {
data.into()
}
#[must_use]
pub const fn content_type(&self) -> ContentType {
self.content_type
}
pub const fn set_content_type(&mut self, content_type: ContentType) -> &mut Self {
self.content_type = content_type;
self
}
#[must_use]
pub fn is_binary(&self) -> bool {
self.content_type == ContentType::Binary
}
#[must_use]
pub fn is_text(&self) -> bool {
self.content_type == ContentType::Text
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.body
}
pub const fn as_mut_bytes(&mut self) -> &mut Vec<u8> {
&mut self.body
}
#[must_use]
pub fn into_bytes(self) -> Vec<u8> {
self.body
}
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
self.body.clone()
}
#[must_use]
pub fn to_base64(&self) -> String {
BASE64.encode(&self.body)
}
pub fn from_base64(encoded: &str) -> Result<Self, DecodeError> {
let decoded = BASE64.decode(encoded)?;
Ok(Self { body: decoded, content_type: ContentType::Binary })
}
pub fn clear(&mut self) {
self.body.clear();
}
#[must_use]
pub fn empty() -> Self {
Self { body: Vec::new(), ..Default::default() }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.body.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.body.len()
}
pub fn push(&mut self, byte: u8) {
self.body.push(byte);
}
pub fn push_bytes(&mut self, bytes: &[u8]) {
self.body.extend_from_slice(bytes);
}
pub fn push_char(&mut self, char: &char) {
let mut bytes = [0; 4];
let used = char.encode_utf8(&mut bytes).len();
#[expect(clippy::indexing_slicing, reason = "Infallible")]
self.body.extend(&bytes[..used]);
}
pub fn push_str(&mut self, string: &str) {
self.body.extend_from_slice(string.as_bytes());
}
}
impl Add<&[u8]> for UnpackedResponseBody {
type Output = Self;
fn add(mut self, rhs: &[u8]) -> Self {
self.push_bytes(rhs);
self
}
}
impl<const N: usize> Add<&[u8; N]> for UnpackedResponseBody {
type Output = Self;
fn add(mut self, rhs: &[u8; N]) -> Self {
self.push_bytes(rhs);
self
}
}
impl Add<char> for UnpackedResponseBody {
type Output = Self;
fn add(mut self, rhs: char) -> Self {
self.push_char(&rhs);
self
}
}
impl Add<&char> for UnpackedResponseBody {
type Output = Self;
fn add(mut self, rhs: &char) -> Self {
self.push_char(rhs);
self
}
}
impl Add<&str> for UnpackedResponseBody {
type Output = Self;
fn add(mut self, rhs: &str) -> Self {
self.push_str(rhs);
self
}
}
impl Add<String> for UnpackedResponseBody {
type Output = Self;
fn add(mut self, rhs: String) -> Self {
self.push_str(&rhs);
self
}
}
impl Add<&String> for UnpackedResponseBody {
type Output = Self;
fn add(mut self, rhs: &String) -> Self {
self.push_str(rhs);
self
}
}
impl Add<Box<str>> for UnpackedResponseBody {
type Output = Self;
fn add(mut self, rhs: Box<str>) -> Self::Output {
self.push_str(&rhs);
self
}
}
impl<'a> Add<Cow<'a, str>> for UnpackedResponseBody {
type Output = Self;
fn add(mut self, rhs: Cow<'a, str>) -> Self::Output {
self.push_str(&rhs);
self
}
}
impl Add<u8> for UnpackedResponseBody {
type Output = Self;
fn add(mut self, rhs: u8) -> Self {
self.push(rhs);
self
}
}
impl Add<Vec<u8>> for UnpackedResponseBody {
type Output = Self;
fn add(mut self, rhs: Vec<u8>) -> Self {
self.push_bytes(&rhs);
self
}
}
impl Add<&Vec<u8>> for UnpackedResponseBody {
type Output = Self;
fn add(mut self, rhs: &Vec<u8>) -> Self {
self.push_bytes(rhs);
self
}
}
impl Add<Self> for UnpackedResponseBody {
type Output = Self;
fn add(mut self, rhs: Self) -> Self {
self.push_bytes(&rhs.body);
self
}
}
impl Add<&Self> for UnpackedResponseBody {
type Output = Self;
fn add(mut self, rhs: &Self) -> Self {
self.push_bytes(rhs.as_bytes());
self
}
}
impl AddAssign<&[u8]> for UnpackedResponseBody {
fn add_assign(&mut self, rhs: &[u8]) {
self.push_bytes(rhs);
}
}
impl<const N: usize> AddAssign<&[u8; N]> for UnpackedResponseBody {
fn add_assign(&mut self, rhs: &[u8; N]) {
self.push_bytes(rhs);
}
}
impl AddAssign<char> for UnpackedResponseBody {
fn add_assign(&mut self, rhs: char) {
self.push_char(&rhs);
}
}
impl AddAssign<&char> for UnpackedResponseBody {
fn add_assign(&mut self, rhs: &char) {
self.push_char(rhs);
}
}
impl AddAssign<&str> for UnpackedResponseBody {
fn add_assign(&mut self, rhs: &str) {
self.push_str(rhs);
}
}
impl AddAssign<String> for UnpackedResponseBody {
fn add_assign(&mut self, rhs: String) {
self.push_str(&rhs);
}
}
impl AddAssign<&String> for UnpackedResponseBody {
fn add_assign(&mut self, rhs: &String) {
self.push_str(rhs);
}
}
impl AddAssign<Box<str>> for UnpackedResponseBody {
fn add_assign(&mut self, rhs: Box<str>) {
self.push_str(&rhs);
}
}
impl<'a> AddAssign<Cow<'a, str>> for UnpackedResponseBody {
fn add_assign(&mut self, rhs: Cow<'a, str>){
self.push_str(&rhs);
}
}
impl AddAssign<u8> for UnpackedResponseBody {
fn add_assign(&mut self, rhs: u8) {
self.push(rhs);
}
}
impl AddAssign<Vec<u8>> for UnpackedResponseBody {
fn add_assign(&mut self, rhs: Vec<u8>) {
self.push_bytes(&rhs);
}
}
impl AddAssign<&Vec<u8>> for UnpackedResponseBody {
fn add_assign(&mut self, rhs: &Vec<u8>) {
self.push_bytes(rhs);
}
}
impl AddAssign<Self> for UnpackedResponseBody {
fn add_assign(&mut self, rhs: Self) {
self.push_bytes(&rhs.body);
}
}
impl AddAssign<&Self> for UnpackedResponseBody {
fn add_assign(&mut self, rhs: &Self) {
self.push_bytes(rhs.as_bytes());
}
}
impl AsMut<[u8]> for UnpackedResponseBody {
fn as_mut(&mut self) -> &mut [u8] {
self.as_mut_bytes()
}
}
impl AsRef<[u8]> for UnpackedResponseBody {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
impl Clone for UnpackedResponseBody {
fn clone(&self) -> Self {
Self { body: self.body.clone(), ..Default::default() }
}
fn clone_from(&mut self, source: &Self) {
self.body.clone_from(&source.body);
}
}
impl Debug for UnpackedResponseBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UnpackedResponseBody")
.field("body", &self.to_string())
.field("content_type", &self.content_type)
.finish()
}
}
impl Display for UnpackedResponseBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let body = match self.content_type {
ContentType::Text => String::from_utf8_lossy(&self.body),
ContentType::Binary => Cow::Owned(self.to_base64()),
};
write!(f, "{body}")
}
}
impl From<&[u8]> for UnpackedResponseBody {
fn from(b: &[u8]) -> Self {
Self { body: b.to_vec(), ..Default::default() }
}
}
impl<const N: usize> From<&[u8; N]> for UnpackedResponseBody {
fn from(b: &[u8; N]) -> Self {
Self { body: b.to_vec(), ..Default::default() }
}
}
impl From<char> for UnpackedResponseBody {
fn from(c: char) -> Self {
let mut bytes = [0; 4];
let used = c.encode_utf8(&mut bytes).len();
#[expect(clippy::indexing_slicing, reason = "Infallible")]
Self { body: bytes[..used].to_vec(), ..Default::default() }
}
}
impl From<&char> for UnpackedResponseBody {
fn from(c: &char) -> Self {
Self::from(c.to_owned())
}
}
#[cfg(feature = "axum")]
impl From<AxumBody> for UnpackedResponseBody {
fn from(b: AxumBody) -> Self {
Self { body: executor::block_on(to_bytes(b, usize::MAX)).unwrap_or_default().to_vec(), ..Default::default() }
}
}
impl From<Full<Bytes>> for UnpackedResponseBody {
fn from(b: Full<Bytes>) -> Self {
let collected = b.collect().now_or_never()
.unwrap_or_else(|| Ok(Collected::default()))
.unwrap_or_else(|_| Collected::default())
;
Self { body: collected.to_bytes().to_vec(), ..Default::default() }
}
}
impl From<Incoming> for UnpackedResponseBody {
fn from(b: Incoming) -> Self {
let collected = executor::block_on(b.collect()).unwrap_or_else(|_| Collected::default());
Self { body: collected.to_bytes().to_vec(), ..Default::default() }
}
}
impl From<Json> for UnpackedResponseBody {
fn from(j: Json) -> Self {
Self { body: j.to_string().into_bytes(), ..Default::default() }
}
}
impl From<&Json> for UnpackedResponseBody {
fn from(j: &Json) -> Self {
Self { body: j.to_string().into_bytes(), ..Default::default() }
}
}
impl From<&str> for UnpackedResponseBody {
fn from(s: &str) -> Self {
Self { body: s.to_owned().as_bytes().to_vec(), ..Default::default() }
}
}
impl From<&mut str> for UnpackedResponseBody {
fn from(s: &mut str) -> Self {
Self { body: s.to_owned().as_bytes().to_vec(), ..Default::default() }
}
}
impl From<String> for UnpackedResponseBody {
fn from(s: String) -> Self {
Self { body: s.into_bytes(), ..Default::default() }
}
}
impl From<&String> for UnpackedResponseBody {
fn from(s: &String) -> Self {
Self { body: s.as_str().as_bytes().to_vec(), ..Default::default() }
}
}
impl From<Box<str>> for UnpackedResponseBody {
fn from(s: Box<str>) -> Self {
Self { body: s.into_string().into_bytes(), ..Default::default() }
}
}
impl<'a> From<Cow<'a, str>> for UnpackedResponseBody {
fn from(s: Cow<'a, str>) -> Self {
Self { body: s.into_owned().into_bytes(), ..Default::default() }
}
}
impl From<u8> for UnpackedResponseBody {
fn from(c: u8) -> Self {
Self { body: Vec::from([c]), ..Default::default() }
}
}
impl From<Vec<u8>> for UnpackedResponseBody {
fn from(v: Vec<u8>) -> Self {
Self { body: v, ..Default::default() }
}
}
impl From<&Vec<u8>> for UnpackedResponseBody {
fn from(v: &Vec<u8>) -> Self {
Self { body: v.clone(), ..Default::default() }
}
}
impl FromStr for UnpackedResponseBody {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self { body: s.as_bytes().to_vec(), ..Default::default() })
}
}
impl PartialEq for UnpackedResponseBody {
fn eq(&self, other: &Self) -> bool {
self.body == other.body
}
}
impl Serialize for UnpackedResponseBody {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl <'de> Deserialize<'de> for UnpackedResponseBody {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let string = String::deserialize(deserializer)?;
#[expect(clippy::option_if_let_else, reason = "Using map_or_else() here would not be as clear, and no more concise")]
match BASE64.decode(&string) {
Ok(decoded) => Ok(Self { body: decoded, content_type: ContentType::Binary }),
Err(_) => Ok(Self { body: string.into_bytes(), content_type: ContentType::Text }),
}
}
}
impl Write for UnpackedResponseBody {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.push_str(s);
Ok(())
}
}
pub trait ResponseExt {
fn unpack(&mut self) -> Result<UnpackedResponse, ResponseError>;
}
impl ResponseExt for Response<()> {
fn unpack(&mut self) -> Result<UnpackedResponse, ResponseError> {
Ok(convert_response(self.status(), self.headers(), &Bytes::new()))
}
}
#[cfg(feature = "axum")]
impl ResponseExt for Response<AxumBody> {
fn unpack(&mut self) -> Result<UnpackedResponse, ResponseError> {
let status = self.status();
let headers = self.headers().clone();
let bytes = executor::block_on(to_bytes(mem::replace(self.body_mut(), AxumBody::empty()), usize::MAX))
.map_err(|e| ResponseError::ConversionError(Box::new(e)))?
;
Ok(convert_response(status, &headers, &bytes))
}
}
impl ResponseExt for Response<Full<Bytes>> {
fn unpack(&mut self) -> Result<UnpackedResponse, ResponseError> {
let collected = self.body().clone().collect().now_or_never()
.unwrap_or_else(|| Ok(Collected::default()))
.map_err(|e| ResponseError::ConversionError(Box::new(e)))?
;
Ok(convert_response(self.status(), self.headers(), &collected.to_bytes()))
}
}
impl ResponseExt for Response<Incoming> {
fn unpack(&mut self) -> Result<UnpackedResponse, ResponseError> {
let collected = executor::block_on(self.body_mut().collect())
.map_err(|e| ResponseError::ConversionError(Box::new(e)))?
;
Ok(convert_response(self.status(), self.headers(), &collected.to_bytes()))
}
}
impl ResponseExt for Response<String> {
fn unpack(&mut self) -> Result<UnpackedResponse, ResponseError> {
Ok(convert_response(self.status(), self.headers(), &Bytes::from(self.body().clone())))
}
}
fn convert_headers(headermap: &HeaderMap<HeaderValue>) -> Vec<UnpackedResponseHeader> {
let mut headers = vec![];
#[expect(clippy::shadow_reuse, reason = "Clear purpose")]
for (name, value) in headermap {
let name = name.as_str().to_owned();
let value = String::from_utf8_lossy(value.as_bytes()).into_owned();
headers.push(UnpackedResponseHeader { name, value });
}
headers.sort_by(|a, b| {
match a.name.cmp(&b.name) {
Ordering::Equal => a.value.cmp(&b.value),
Ordering::Greater => Ordering::Greater,
Ordering::Less => Ordering::Less,
}
});
headers
}
fn convert_response(
status: StatusCode,
headers: &HeaderMap<HeaderValue>,
body: &Bytes,
) -> UnpackedResponse {
UnpackedResponse {
status,
headers: convert_headers(headers),
body: UnpackedResponseBody { body: body.to_vec(), ..Default::default() },
}
}
#[expect(clippy::trivially_copy_pass_by_ref, reason = "Needs to match trait")]
fn serialize_status_code<S>(status_code: &StatusCode, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u16(status_code.as_u16())
}
fn deserialize_status_code<'de, D>(deserializer: D) -> Result<StatusCode, D::Error>
where
D: Deserializer<'de>,
{
let status_code_value: u16 = Deserialize::deserialize(deserializer)?;
let status_code = StatusCode::from_u16(status_code_value).map_err(DeError::custom)?;
Ok(status_code)
}