feat: custom error pages support
Diff
README.md | 2 +-
src/bin/server.rs | 142 +++++++++++++++++++++++++++++++++------------------
src/core/cache.rs | 7 +++-
src/core/config.rs | 16 ++++++-
src/core/helpers.rs | 14 ++++-
src/core/rejection.rs | 39 +++++++++-----
6 files changed, 157 insertions(+), 63 deletions(-)
@@ -11,7 +11,7 @@ For stable `v1` and contributions please refer to [1.x](https://github.com/josel
- Built with [Rust](https://rust-lang.org) which is focused on [safety, speed, and concurrency](https://kornel.ski/rust-c-speed).
- Memory safety and very reduced CPU and RAM overhead.
- Blazing fast static files-serving powered by [Warp](https://github.com/seanmonstar/warp/) `v0.2` ([Hyper](https://github.com/hyperium/hyper/) `v0.13`), [Tokio](https://github.com/tokio-rs/tokio) `v0.2` and a set of [awesome crates](./Cargo.toml).
- Blazing fast static files-serving powered by [Warp](https://github.com/seanmonstar/warp/) `v0.3` ([Hyper](https://github.com/hyperium/hyper/) `v0.14`), [Tokio](https://github.com/tokio-rs/tokio) `v1` and a set of [awesome crates](./Cargo.toml).
- Suitable for lightweight [GNU/Linux Docker containers](https://hub.docker.com/r/joseluisq/static-web-server/tags). It's a fully __5MB__ static binary thanks to [Rust and Musl libc](https://doc.rust-lang.org/edition-guide/rust-2018/platform-and-target-support/musl-support-for-fully-static-binaries.html).
- Opt-in GZip, Deflate and Brotli compression for text-based web files only.
- Compression on demand via [Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) header.
@@ -16,71 +16,117 @@ use self::static_web_server::core::*;
async fn server(opts: config::Options) -> Result {
logger::init(&opts.log_level)?;
let root_dir = helpers::get_valid_dirpath(opts.root)?;
let page404 = helpers::read_file_content(opts.page404.as_ref());
let page50x = helpers::read_file_content(opts.page50x.as_ref());
let page404_a = page404.clone();
let page50x_a = page50x.clone();
let public_head = warp::head().and(
warp::fs::dir(opts.root.clone())
warp::fs::dir(root_dir.clone())
.map(cache::control_headers)
.with(warp::trace::request())
.recover(rejection::handle_rejection),
.recover(move |r| {
let page404_a = page404_a.clone();
let page50x_a = page50x_a.clone();
async move { rejection::handle_rejection(page404_a, page50x_a, r).await }
}),
);
let page404_b = page404.clone();
let page50x_b = page50x.clone();
let public_get_default = warp::get().and(
warp::fs::dir(opts.root.clone())
warp::fs::dir(root_dir.clone())
.map(cache::control_headers)
.with(warp::trace::request())
.recover(rejection::handle_rejection),
.recover(move |r| {
let page404_b = page404_b.clone();
let page50x_b = page50x_b.clone();
async move { rejection::handle_rejection(page404_b, page50x_b, r).await }
}),
);
let host = opts.host.parse::<std::net::IpAddr>()?;
let port = opts.port;
let accept_encoding = |v: &'static str| warp::header::contains("accept-encoding", v);
let page404_c = page404.clone();
let page50x_c = page50x.clone();
match opts.compression.as_ref() {
"brotli" => tokio::task::spawn(
warp::serve(
public_head.or(warp::get()
.and(accept_encoding("br"))
.and(
warp::fs::dir(opts.root.clone())
.map(cache::control_headers)
.with(warp::trace::request())
.with(warp::compression::brotli(true))
.recover(rejection::handle_rejection),
)
.or(public_get_default)),
"brotli" => {
tokio::task::spawn(
warp::serve(
public_head.or(warp::get()
.and(cache::accept_encoding("br"))
.and(
warp::fs::dir(root_dir.clone())
.map(cache::control_headers)
.with(warp::trace::request())
.with(warp::compression::brotli(true))
.recover(move |r| {
let page404_c = page404_c.clone();
let page50x_c = page50x_c.clone();
async move {
rejection::handle_rejection(page404_c, page50x_c, r).await
}
}),
)
.or(public_get_default)),
)
.run((host, port)),
)
.run((host, port)),
),
"deflate" => tokio::task::spawn(
warp::serve(
public_head.or(warp::get()
.and(accept_encoding("deflate"))
.and(
warp::fs::dir(opts.root.clone())
.map(cache::control_headers)
.with(warp::trace::request())
.with(warp::compression::deflate(true))
.recover(rejection::handle_rejection),
)
.or(public_get_default)),
}
"deflate" => {
tokio::task::spawn(
warp::serve(
public_head.or(warp::get()
.and(cache::accept_encoding("deflate"))
.and(
warp::fs::dir(root_dir.clone())
.map(cache::control_headers)
.with(warp::trace::request())
.with(warp::compression::deflate(true))
.recover(move |r| {
let page404_c = page404_c.clone();
let page50x_c = page50x_c.clone();
async move {
rejection::handle_rejection(page404_c, page50x_c, r).await
}
}),
)
.or(public_get_default)),
)
.run((host, port)),
)
.run((host, port)),
),
"gzip" => tokio::task::spawn(
warp::serve(
public_head.or(warp::get()
.and(accept_encoding("gzip"))
.and(
warp::fs::dir(opts.root.clone())
.map(cache::control_headers)
.with(warp::trace::request())
.with(warp::compression::gzip(true))
.recover(rejection::handle_rejection),
)
.or(public_get_default)),
}
"gzip" => {
tokio::task::spawn(
warp::serve(
public_head.or(warp::get()
.and(cache::accept_encoding("gzip"))
.and(
warp::fs::dir(root_dir.clone())
.map(cache::control_headers)
.with(warp::trace::request())
.with(warp::compression::gzip(true))
.recover(move |r| {
let page404_c = page404_c.clone();
let page50x_c = page50x_c.clone();
async move {
rejection::handle_rejection(page404_c, page50x_c, r).await
}
}),
)
.or(public_get_default)),
)
.run((host, port)),
)
.run((host, port)),
),
}
_ => tokio::task::spawn(warp::serve(public_head.or(public_get_default)).run((host, port))),
};
@@ -36,3 +36,10 @@ pub fn control_headers(res: warp::fs::File) -> warp::reply::WithHeader<warp::fs:
fn duration(n: u64) -> u32 {
std::cmp::min(n, u32::MAX as u64) as u32
}
pub fn accept_encoding(
val: &'static str,
) -> impl warp::Filter<Extract = (), Error = warp::Rejection> + Copy {
warp::header::contains("accept-encoding", val)
}
@@ -27,6 +27,22 @@ pub struct Options {
pub root: String,
#[structopt(
long,
default_value = "./public/50x.html",
env = "SERVER_ERROR_PAGE_50X"
)]
pub page50x: String,
#[structopt(
long,
default_value = "./public/404.html",
env = "SERVER_ERROR_PAGE_404"
)]
pub page404: String,
#[structopt(long, short = "c", default_value = "gzip", env = "SERVER_COMPRESSION")]
@@ -1,3 +1,4 @@
use std::fs;
use std::path::{Path, PathBuf};
use super::Result;
@@ -8,8 +9,8 @@ where
PathBuf: From<P>,
{
match PathBuf::from(path) {
v if !v.exists() => bail!("path \"{:?}\" was not found", &v),
v if !v.is_dir() => bail!("path \"{:?}\" is not a directory", &v),
v if !v.exists() => bail!("path \"{:?}\" was not found or inaccessible", &v),
v if !v.is_dir() => bail!("path \"{:?}\" is not a valid directory", &v),
v => Ok(v),
}
}
@@ -25,3 +26,12 @@ where
_ => bail!("directory name for path \"{:?}\" was not determined", path),
}
}
pub fn read_file_content(p: &str) -> String {
if !p.is_empty() && Path::new(p).exists() {
return fs::read_to_string(p).unwrap_or(String::new());
}
String::new()
}
@@ -4,22 +4,37 @@ use warp::http::StatusCode;
use warp::{Rejection, Reply};
pub async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> {
pub async fn handle_rejection(
page_404: String,
page_50x: String,
err: Rejection,
) -> Result<impl Reply, Infallible> {
let mut content = String::new();
let code = if err.is_not_found() {
content = page_404;
StatusCode::NOT_FOUND
} else if err
} else {
if err
.find::<warp::filters::body::BodyDeserializeError>()
.is_some()
{
StatusCode::BAD_REQUEST
} else if err.find::<warp::reject::MethodNotAllowed>().is_some() {
StatusCode::METHOD_NOT_ALLOWED
} else {
StatusCode::INTERNAL_SERVER_ERROR
{
StatusCode::BAD_REQUEST
} else if err.find::<warp::reject::MethodNotAllowed>().is_some() {
StatusCode::METHOD_NOT_ALLOWED
} else if err.find::<warp::reject::UnsupportedMediaType>().is_some() {
StatusCode::UNSUPPORTED_MEDIA_TYPE
} else {
content = page_50x;
StatusCode::INTERNAL_SERVER_ERROR
}
};
let content = format!(
"<html><head><title>{}</title></head><body><center><h1>{}</h1></center></body></html>",
code, code
);
if content.is_empty() {
content = format!(
"<html><head><title>{}</title></head><body><center><h1>{}</h1></center></body></html>",
code, code
);
}
Ok(warp::reply::with_status(warp::reply::html(content), code))
}