use super::cookie::{CookieOptions, sanitize_cookie};
use super::rejection::MappedRefusal;
use crate::RuntimeError;
use bytes::Bytes;
use serde::Serialize;
use std::borrow::Cow;
use std::fmt;
use std::sync::OnceLock;
pub type HeaderPair = (Cow<'static, str>, Cow<'static, str>);
enum BodyStore {
Text(Box<str>),
Raw {
bytes: Bytes,
text_cache: OnceLock<Box<str>>,
},
Empty,
}
pub trait IntoResponse {
fn into_response(self) -> Result<Response, RuntimeError>;
}
impl IntoResponse for Response {
fn into_response(self) -> Result<Response, RuntimeError> {
Ok(self)
}
}
impl IntoResponse for Result<Response, RuntimeError> {
fn into_response(self) -> Result<Response, RuntimeError> {
self
}
}
pub(super) enum ResponseProvenance {
Application,
Mapped(Box<MappedRefusal>),
Gate,
}
impl ResponseProvenance {
pub(super) fn is_gate_passthrough(&self) -> bool {
match self {
Self::Gate => true,
Self::Application | Self::Mapped(_) => false,
}
}
pub(super) fn into_refusal(self) -> Option<Box<MappedRefusal>> {
match self {
Self::Mapped(refusal) => Some(refusal),
Self::Application | Self::Gate => None,
}
}
fn label(&self) -> &'static str {
match self {
Self::Application => "application",
Self::Mapped(_) => "mapped",
Self::Gate => "gate",
}
}
}
const CONTENT_TYPE: &str = "Content-Type";
pub(super) struct UnrepresentableResponse {
error: hyper::http::Error,
content_type: Option<Box<str>>,
}
impl UnrepresentableResponse {
fn new(error: hyper::http::Error, headers: &[HeaderPair]) -> Self {
Self {
error,
content_type: representable_content_type(headers),
}
}
pub(super) fn content_type(&self) -> Option<&str> {
self.content_type.as_deref()
}
pub(super) fn into_error(self) -> hyper::http::Error {
self.error
}
}
fn representable_content_type(headers: &[HeaderPair]) -> Option<Box<str>> {
headers
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case(CONTENT_TYPE))
.filter(|(_, value)| hyper::header::HeaderValue::from_str(value).is_ok())
.map(|(_, value)| Box::from(value.as_ref()))
}
fn wire_bytes(body: BodyStore) -> Bytes {
match body {
BodyStore::Text(text) => Bytes::from(String::from(text)),
BodyStore::Raw { bytes, .. } => bytes,
BodyStore::Empty => Bytes::new(),
}
}
pub(super) fn validate_status(status: u16) -> Result<(), RuntimeError> {
match (100..=599).contains(&status) {
true => Ok(()),
false => Err(RuntimeError::InvalidArgument(
format!("invalid HTTP status code: {status}").into_boxed_str(),
)),
}
}
impl fmt::Debug for Response {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (body_type, body_len) = match &self.body {
BodyStore::Text(text) => ("text", text.len()),
BodyStore::Raw { bytes, .. } => ("raw", bytes.len()),
BodyStore::Empty => ("empty", 0),
};
f.debug_struct("Response")
.field("status", &self.status)
.field("provenance", &self.provenance.label())
.field("header_count", &self.headers.len())
.field("body_type", &body_type)
.field("body_length", &body_len)
.finish()
}
}
pub struct Response {
status: u16,
body: BodyStore,
headers: Vec<HeaderPair>,
provenance: ResponseProvenance,
}
impl Response {
pub(crate) fn new(status: u16, body: Bytes, headers: Vec<HeaderPair>) -> Self {
Self {
status,
body: BodyStore::Raw {
bytes: body,
text_cache: OnceLock::new(),
},
headers,
provenance: ResponseProvenance::Application,
}
}
fn build_text(status: u16, body: &str) -> Self {
Self {
status,
body: BodyStore::Text(body.into()),
headers: vec![(Cow::Borrowed("Content-Type"), Cow::Borrowed("text/plain"))],
provenance: ResponseProvenance::Application,
}
}
fn build_empty(status: u16) -> Self {
Self {
status,
body: BodyStore::Empty,
headers: Vec::new(),
provenance: ResponseProvenance::Application,
}
}
fn build_bytes(status: u16, data: impl Into<Bytes>) -> Self {
Self {
status,
body: BodyStore::Raw {
bytes: data.into(),
text_cache: OnceLock::new(),
},
headers: vec![
(
Cow::Borrowed("Content-Type"),
Cow::Borrowed("application/octet-stream"),
),
(
Cow::Borrowed("X-Content-Type-Options"),
Cow::Borrowed("nosniff"),
),
],
provenance: ResponseProvenance::Application,
}
}
pub fn text(status: u16, body: &str) -> Result<Self, RuntimeError> {
validate_status(status)?;
Ok(Self::build_text(status, body))
}
pub fn empty(status: u16) -> Result<Self, RuntimeError> {
validate_status(status)?;
Ok(Self::build_empty(status))
}
pub fn json(status: u16, value: &impl Serialize) -> Result<Self, RuntimeError> {
validate_status(status)?;
let body = serde_json::to_vec(value).map_err(|e| {
RuntimeError::InvalidArgument(
format!("json serialization failed: {e}").into_boxed_str(),
)
})?;
Ok(Self {
status,
body: BodyStore::Raw {
bytes: Bytes::from(body),
text_cache: OnceLock::new(),
},
headers: vec![(
Cow::Borrowed("Content-Type"),
Cow::Borrowed("application/json"),
)],
provenance: ResponseProvenance::Application,
})
}
pub fn bytes(status: u16, data: impl Into<Bytes>) -> Result<Self, RuntimeError> {
validate_status(status)?;
Ok(Self::build_bytes(status, data))
}
pub(crate) fn text_raw(status: u16, body: &str) -> Self {
Self::build_text(status, body)
}
pub(crate) fn empty_raw(status: u16) -> Self {
Self::build_empty(status)
}
pub(crate) fn bytes_raw(status: u16, data: impl Into<Bytes>) -> Self {
Self::build_bytes(status, data)
}
pub fn with_header(self, name: &str, value: &str) -> Self {
self.with_pair(Cow::Owned(name.to_owned()), Cow::Owned(value.to_owned()))
}
pub(super) fn with_static_header(self, name: &'static str, value: Cow<'static, str>) -> Self {
self.with_pair(Cow::Borrowed(name), value)
}
pub(super) fn with_pair(mut self, name: Cow<'static, str>, value: Cow<'static, str>) -> Self {
self.headers.push((name, value));
self
}
pub fn with_content_type(self, content_type: &str) -> Self {
self.with_replaced_header(CONTENT_TYPE, Cow::Owned(content_type.to_owned()))
}
pub(super) fn with_replaced_header(self, name: &'static str, value: Cow<'static, str>) -> Self {
self.without_header(name)
.with_pair(Cow::Borrowed(name), value)
}
pub(super) fn without_header(mut self, name: &str) -> Self {
self.headers
.retain(|(key, _)| !key.eq_ignore_ascii_case(name));
self
}
pub(super) fn without_headers(mut self, names: &[&str]) -> Self {
self.headers
.retain(|(key, _)| !names.iter().any(|name| key.eq_ignore_ascii_case(name)));
self
}
pub fn set_cookie(self, name: &str, value: &str) -> Self {
let header_value = format!("{}={}", sanitize_cookie(name), sanitize_cookie(value));
self.with_static_header("Set-Cookie", Cow::Owned(header_value))
}
pub fn set_cookie_with(self, name: &str, value: &str, options: &CookieOptions) -> Self {
let header_value = options.format_header(name, value);
self.with_static_header("Set-Cookie", Cow::Owned(header_value))
}
pub(crate) fn strip_body(self) -> Self {
Self {
body: BodyStore::Empty,
..self
}
}
#[must_use]
pub(super) fn mark_mapped(self, refusal: MappedRefusal) -> Self {
Self {
provenance: ResponseProvenance::Mapped(Box::new(refusal)),
..self
}
}
#[must_use]
pub(super) fn mark_gate(self) -> Self {
Self {
provenance: ResponseProvenance::Gate,
..self
}
}
pub(super) fn provenance(&self) -> &ResponseProvenance {
&self.provenance
}
pub fn status(&self) -> u16 {
self.status
}
pub fn body(&self) -> &str {
match &self.body {
BodyStore::Text(text) => text,
BodyStore::Raw { bytes, text_cache } => super::encoding::lossy_text(bytes, text_cache),
BodyStore::Empty => "",
}
}
pub fn body_bytes(&self) -> &[u8] {
match &self.body {
BodyStore::Text(text) => text.as_bytes(),
BodyStore::Raw { bytes, .. } => bytes,
BodyStore::Empty => &[],
}
}
pub fn headers(&self) -> &[HeaderPair] {
&self.headers
}
pub(super) fn into_wire(
self,
) -> (
ResponseProvenance,
Result<hyper::Response<http_body_util::Full<Bytes>>, UnrepresentableResponse>,
) {
let Self {
status,
body,
headers,
provenance,
} = self;
let mut builder = hyper::Response::builder().status(status);
for (name, value) in &headers {
builder = builder.header(name.as_ref(), value.as_ref());
}
let converted = builder
.body(http_body_util::Full::new(wire_bytes(body)))
.map_err(|error| UnrepresentableResponse::new(error, &headers));
(provenance, converted)
}
}