use crate::http::cookie::SetCookie;
use crate::http::headers::{Headers, HttpHeader, HttpHeaderName};
use crate::http::status::StatusCode;
use crate::http::method::HttpMethod;
use crate::http::request::HttpVersion;
use crate::http::response_body::ResponseBody;
use crate::stream::ConnectionStreamWrite;
use crate::tii_error::{TiiResult, UserError};
use crate::{EntitySerializer, MimeTypeWithCharset};
use std::fmt::Debug;
use std::io::{Read, Seek};
use std::{io, mem};
#[derive(Debug)]
pub struct Response {
pub status_code: StatusCode,
pub(crate) headers: Headers,
pub body: Option<ResponseBody>,
pub omit_body: bool,
}
impl Response {
pub fn new(status_code: impl Into<StatusCode>) -> Self {
let status_code = status_code.into();
Self { status_code, headers: Headers::new(), body: None, omit_body: false }
}
pub fn ok(bytes: impl Into<ResponseBody>, mime: impl Into<MimeTypeWithCharset>) -> Response {
Self::new(StatusCode::OK)
.with_body(bytes.into())
.with_header_unchecked("Content-Type", mime.into())
}
pub fn ok_entity<T: Debug + Send + 'static>(
entity: T,
serializer: impl EntitySerializer<T>,
mime: impl Into<MimeTypeWithCharset>,
) -> Response {
Self::ok(ResponseBody::from_entity(entity, serializer), mime)
}
pub fn try_ok<E>(
bytes: impl TryInto<ResponseBody, Error = E>,
mime: impl Into<MimeTypeWithCharset>,
) -> Result<Response, E> {
Ok(Self::ok(bytes.try_into()?, mime))
}
pub fn created(bytes: impl Into<ResponseBody>, mime: impl Into<MimeTypeWithCharset>) -> Response {
Self::new(StatusCode::Created)
.with_body(bytes.into())
.with_header_unchecked("Content-Type", mime.into())
}
pub fn created_entity<T: Debug + Send + 'static>(
entity: T,
serializer: impl EntitySerializer<T>,
mime: impl Into<MimeTypeWithCharset>,
) -> Response {
Self::created(ResponseBody::from_entity(entity, serializer), mime)
}
pub fn accepted(
bytes: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Response {
Self::new(StatusCode::Created)
.with_body(bytes.into())
.with_header_unchecked("Content-Type", mime.into())
}
pub fn non_authoritative(
bytes: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Response {
Self::new(StatusCode::NonAuthoritative)
.with_body(bytes.into())
.with_header_unchecked("Content-Type", mime.into())
}
pub fn no_content() -> Response {
Self::new(StatusCode::NoContent)
}
pub fn reset_content() -> Response {
Self::new(StatusCode::ResetContent)
}
pub fn partial_content(
body: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Response {
Self::new(StatusCode::PartialContent)
.with_body(body.into())
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
}
pub fn multiple_choices(
body: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Response {
Self::new(StatusCode::MultipleChoices)
.with_body(body.into())
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
}
pub fn multiple_choices_no_body() -> Response {
Self::new(StatusCode::MultipleChoices)
}
pub fn moved_permanently(
location: impl ToString,
body: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Response {
Self::new(StatusCode::MovedPermanently)
.with_header_unchecked(HttpHeaderName::Location, location)
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
.with_body(body.into())
}
pub fn moved_permanently_no_body(location: impl ToString) -> Response {
Self::new(StatusCode::MovedPermanently)
.with_header_unchecked(HttpHeaderName::Location, location)
}
pub fn found(
location: impl ToString,
body: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Response {
Self::new(StatusCode::Found)
.with_header_unchecked(HttpHeaderName::Location, location)
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
.with_body(body.into())
}
pub fn found_no_body(location: impl ToString) -> Response {
Self::new(StatusCode::Found).with_header_unchecked(HttpHeaderName::Location, location)
}
pub fn see_other(
location: impl ToString,
body: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Response {
Self::new(StatusCode::SeeOther)
.with_header_unchecked(HttpHeaderName::Location, location)
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
.with_body(body.into())
}
pub fn see_other_no_body(location: impl ToString) -> Response {
Self::new(StatusCode::SeeOther).with_header_unchecked(HttpHeaderName::Location, location)
}
pub fn not_modified() -> Response {
Self::new(StatusCode::NotModified)
}
pub fn temporary_redirect(
location: impl ToString,
body: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Response {
Self::new(StatusCode::TemporaryRedirect)
.with_header_unchecked(HttpHeaderName::Location, location)
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
.with_body(body.into())
}
pub fn temporary_redirect_no_body(location: impl ToString) -> Response {
Self::new(StatusCode::TemporaryRedirect)
.with_header_unchecked(HttpHeaderName::Location, location)
}
pub fn permanent_redirect(
location: impl ToString,
body: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Response {
Self::new(StatusCode::PermanentRedirect)
.with_body(body.into())
.with_header_unchecked(HttpHeaderName::Location, location)
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
}
pub fn permanent_redirect_no_body(location: impl ToString) -> Response {
Self::new(StatusCode::PermanentRedirect)
.with_header_unchecked(HttpHeaderName::Location, location)
}
pub fn bad_request(
body: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Response {
Self::new(StatusCode::BadRequest)
.with_body(body.into())
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
}
pub fn bad_request_no_body() -> Response {
Self::new(StatusCode::BadRequest)
}
pub fn unauthorized() -> Response {
Self::new(StatusCode::Unauthorized)
}
pub fn payment_required(
body: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Response {
Self::new(StatusCode::PaymentRequired)
.with_body(body.into())
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
}
pub fn payment_required_no_body() -> Response {
Self::new(StatusCode::PaymentRequired)
}
pub fn forbidden(
body: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Response {
Self::new(StatusCode::Forbidden)
.with_body(body.into())
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
}
pub fn forbidden_no_body() -> Response {
Self::new(StatusCode::Forbidden)
}
pub fn not_found(body: impl Into<ResponseBody>, mime: impl Into<MimeTypeWithCharset>) -> Self {
Self::new(StatusCode::NotFound)
.with_body(body.into())
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
}
pub fn not_found_no_body() -> Self {
Self::new(StatusCode::NotFound)
}
pub fn method_not_allowed(allowed_methods: &[HttpMethod]) -> Self {
if allowed_methods.is_empty() {
return Self::new(StatusCode::MethodNotAllowed);
}
let mut buf = String::new();
for method in allowed_methods {
if !buf.is_empty() {
buf += ", ";
}
buf += method.as_str();
}
Self::new(StatusCode::MethodNotAllowed)
.with_header_unchecked(HttpHeaderName::Allow, buf.as_str())
}
pub fn not_acceptable(
body: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Self {
Self::new(StatusCode::NotAcceptable)
.with_body(body.into())
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
}
pub fn not_acceptable_no_body() -> Self {
Self::new(StatusCode::NotAcceptable)
}
pub fn proxy_authentication_required(authenticate: impl ToString) -> Self {
Self::new(StatusCode::ProxyAuthenticationRequired)
.with_header_unchecked(HttpHeaderName::ProxyAuthenticate, authenticate)
}
pub fn request_timeout() -> Self {
Self::new(StatusCode::RequestTimeout)
}
pub fn conflict(body: impl Into<ResponseBody>, mime: impl Into<MimeTypeWithCharset>) -> Self {
Self::new(StatusCode::Conflict)
.with_body(body.into())
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
}
pub fn conflict_no_body() -> Self {
Self::new(StatusCode::Conflict)
}
pub fn gone(body: impl Into<ResponseBody>, mime: impl Into<MimeTypeWithCharset>) -> Self {
Self::new(StatusCode::Gone)
.with_body(body.into())
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
}
pub fn gone_no_body() -> Self {
Self::new(StatusCode::Gone)
}
pub fn length_required(
body: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Self {
Self::new(StatusCode::LengthRequired)
.with_body(body.into())
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
}
pub fn length_required_no_body() -> Self {
Self::new(StatusCode::LengthRequired)
}
pub fn precondition_failed() -> Self {
Self::new(StatusCode::PreconditionFailed)
}
pub fn content_too_large(
body: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Self {
Self::new(StatusCode::ContentTooLarge)
.with_body(body.into())
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
}
pub fn content_too_large_no_body() -> Self {
Self::new(StatusCode::ContentTooLarge)
}
pub fn unsupported_media_type(
body: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Response {
Self::new(StatusCode::UnsupportedMediaType)
.with_body(body.into())
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
}
pub fn unsupported_media_type_no_body() -> Response {
Self::new(StatusCode::UnsupportedMediaType)
}
pub fn internal_server_error(
body: impl Into<ResponseBody>,
mime: impl Into<MimeTypeWithCharset>,
) -> Response {
Self::new(StatusCode::InternalServerError)
.with_body(body.into())
.with_header_unchecked(HttpHeaderName::ContentType, mime.into())
}
pub fn internal_server_error_no_body() -> Response {
Self::new(StatusCode::InternalServerError)
}
pub fn without_body(mut self) -> Self {
self.body = None;
self
}
pub fn with_body(mut self, body: impl Into<ResponseBody>) -> Self {
self.body = Some(body.into());
self
}
pub fn with_body_string<T: AsRef<str>>(mut self, body: T) -> Self {
self.body = Some(ResponseBody::from_string(body.as_ref().to_string()));
self
}
pub fn with_body_vec(mut self, body: Vec<u8>) -> Self {
self.body = Some(ResponseBody::from_data(body));
self
}
pub fn with_body_slice<T: AsRef<[u8]>>(mut self, body: T) -> Self {
self.body = Some(ResponseBody::from_slice(&body));
self
}
pub fn with_body_file<T: Read + Seek + Send + 'static>(mut self, body: T) -> io::Result<Self> {
self.body = Some(ResponseBody::from_file(body)?);
Ok(self)
}
pub fn with_header(mut self, header: impl AsRef<str>, value: impl ToString) -> TiiResult<Self> {
self.add_header(header, value)?;
Ok(self)
}
fn with_header_unchecked(mut self, header: impl AsRef<str>, value: impl ToString) -> Self {
self.headers.push(HttpHeader::new(header, value.to_string()));
self
}
pub fn get_status_code(&self) -> &StatusCode {
&self.status_code
}
pub fn get_status_code_number(&self) -> u16 {
self.status_code.code()
}
pub fn set_status_code(&mut self, status_code: StatusCode) {
self.status_code = status_code;
}
pub fn add_header(&mut self, hdr: impl AsRef<str>, value: impl ToString) -> TiiResult<()> {
match &hdr.as_ref().into() {
HttpHeaderName::ContentLength => {
UserError::ImmutableResponseHeaderModified(HttpHeaderName::ContentLength).into()
}
HttpHeaderName::TransferEncoding => {
UserError::ImmutableResponseHeaderModified(HttpHeaderName::TransferEncoding).into()
}
HttpHeaderName::Trailer => {
UserError::ImmutableResponseHeaderModified(HttpHeaderName::Trailer).into()
}
hdr => {
self.headers.add(hdr, value);
Ok(())
}
}
}
pub fn set_header(&mut self, header: impl AsRef<str>, value: impl ToString) -> TiiResult<()> {
match &header.as_ref().into() {
HttpHeaderName::ContentLength => {
UserError::ImmutableResponseHeaderModified(HttpHeaderName::ContentLength).into()
}
HttpHeaderName::TransferEncoding => {
UserError::ImmutableResponseHeaderModified(HttpHeaderName::TransferEncoding).into()
}
HttpHeaderName::Trailer => {
UserError::ImmutableResponseHeaderModified(HttpHeaderName::Trailer).into()
}
hdr => {
self.headers.set(hdr, value);
Ok(())
}
}
}
pub fn remove_header(&mut self, header: impl AsRef<str>) {
self.headers.remove(header);
}
pub fn get_all_headers(&self) -> impl Iterator<Item = &HttpHeader> {
self.headers.iter()
}
pub fn get_header(&self, name: impl AsRef<str>) -> Option<&str> {
self.headers.get(name)
}
pub fn get_headers(&self, name: impl AsRef<str>) -> Vec<&str> {
self.headers.get_all(name)
}
pub fn with_cookie(mut self, cookie: SetCookie) -> Self {
self.headers.push(cookie.into());
self
}
pub fn set_body(&mut self, body: Option<ResponseBody>) -> Option<ResponseBody> {
mem::replace(&mut self.body, body)
}
pub fn get_body(&self) -> Option<&ResponseBody> {
self.body.as_ref()
}
pub fn get_body_mut(&mut self) -> Option<&mut ResponseBody> {
self.body.as_mut()
}
pub fn write_to<T: ConnectionStreamWrite + ?Sized>(
mut self,
request_id: u128,
version: HttpVersion,
destination: &T,
) -> TiiResult<()> {
if let Some(body) = self.set_body(None) {
let mime = if let Some(mime) = self.get_header(HttpHeaderName::ContentType) {
MimeTypeWithCharset::parse_from_content_type_header(mime)
.unwrap_or(MimeTypeWithCharset::APPLICATION_OCTET_STREAM)
} else {
MimeTypeWithCharset::APPLICATION_OCTET_STREAM
};
self.set_body(Some(body.serialize_entity(&mime)?));
}
if version == HttpVersion::Http09 {
if let Some(body) = self.body {
body.write_to_http(request_id, destination)?;
destination.flush()?;
}
return Ok(());
}
destination.write_all(version.as_net_str().as_bytes())?;
destination.write_all(b" ")?;
destination.write_all(self.status_code.code_as_utf())?;
destination.write_all(b" ")?;
destination.write_all(self.status_code.status_line().as_bytes())?;
for header in self.headers.iter() {
if header.name == HttpHeaderName::ContentLength {
crate::util::unreachable();
}
if header.name == HttpHeaderName::TransferEncoding {
crate::util::unreachable();
}
destination.write_all(b"\r\n")?;
destination.write_all(header.name.to_str().as_bytes())?;
destination.write_all(b": ")?;
destination.write_all(header.value.as_bytes())?;
}
if let Some(body) = self.body {
if body.is_chunked() {
destination.write_all(b"\r\nTransfer-Encoding: chunked\r\n")?;
if let Some(enc) = body.get_content_encoding() {
if self.headers.get(HttpHeaderName::ContentEncoding).is_none() {
destination.write_all(b"Content-Encoding: ")?;
destination.write_all(enc.as_bytes())?;
destination.write_all(b"\r\n")?;
}
}
destination.write_all(b"\r\n")?;
if !self.omit_body {
body.write_to_http(request_id, destination)?;
}
destination.flush()?;
return Ok(());
}
destination.write_all(b"\r\n")?;
if let Some(len) = body.content_length() {
destination.write(format!("Content-Length: {len}\r\n").as_bytes())?;
}
if let Some(enc) = body.get_content_encoding() {
if self.headers.get(HttpHeaderName::ContentEncoding).is_none() {
destination.write_all(b"Content-Encoding: ")?;
destination.write_all(enc.as_bytes())?;
destination.write_all(b"\r\n")?;
}
}
destination.write_all(b"\r\n")?;
if !self.omit_body {
body.write_to_http(request_id, destination)?;
}
destination.flush()?;
return Ok(());
}
destination.write_all(b"\r\nContent-Length: 0\r\n\r\n")?;
destination.flush()?;
Ok(())
}
}