Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve error handling in the warp support. #109

Merged
merged 1 commit into from Feb 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,7 @@ project adheres to

## Unreleased

* Improve error handling in optional warp support.
* Current stable rust is 1.57, MSRV is now 1.46.0.
* Update nom dependency to 7.1.0.
* Update optional rsass to 0.23.0.
Expand Down
1 change: 1 addition & 0 deletions examples/warp03/Cargo.toml
Expand Up @@ -14,3 +14,4 @@ warp = "0.3.0"
mime = "0.3.0"
env_logger = "0.9"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
log = "0.4.14"
153 changes: 113 additions & 40 deletions examples/warp03/src/main.rs
Expand Up @@ -2,7 +2,9 @@
use std::io::{self, Write};
use std::time::{Duration, SystemTime};
use templates::{statics::StaticFile, RenderRucte};
use warp::http::{Response, StatusCode};
use warp::http::response::Builder;
use warp::http::StatusCode;
use warp::reply::Response;
use warp::{path, Filter, Rejection, Reply};

/// Main program: Set up routes and start server.
Expand All @@ -13,35 +15,60 @@ async fn main() {
let routes = warp::get()
.and(
path::end()
.and_then(home_page)
.or(path("static").and(path::param()).and_then(static_file))
.or(path("bad").and_then(bad_handler)),
.then(home_page)
.map(wrap)
.or(path("static")
.and(path::param())
.then(static_file)
.map(wrap))
.or(path("arg")
.and(path::param())
.then(arg_handler)
.map(wrap)),
)
.recover(customize_error);
warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
}

/// Home page handler; just render a template with some arguments.
async fn home_page() -> Result<impl Reply, Rejection> {
Response::builder().html(|o| {
templates::page(o, &[("first", 3), ("second", 7), ("third", 2)])
})
}
type Result<T, E = MyError> = std::result::Result<T, E>;

#[derive(Debug)]
struct SomeError;
impl std::error::Error for SomeError {}
impl warp::reject::Reject for SomeError {}

impl std::fmt::Display for SomeError {
fn fmt(&self, out: &mut std::fmt::Formatter) -> std::fmt::Result {
out.write_str("Some error")
/// An error response is also a response.
///
/// Until <https://github.com/seanmonstar/warp/pull/909> is merged, we
/// need to do this manually, with a `.map(wrap)` after the handlers
/// above.
fn wrap(result: Result<impl Reply, impl Reply>) -> Response {
match result {
Ok(reply) => reply.into_response(),
Err(err) => err.into_response(),
}
}

/// A handler that always gives a server error.
async fn bad_handler() -> Result<StatusCode, Rejection> {
Err(warp::reject::custom(SomeError))
/// Home page handler; just render a template with some arguments.
async fn home_page() -> Result<impl Reply> {
Ok(Builder::new().html(|o| {
templates::page(o, &[("first", 3), ("second", 7), ("third", 2)])
})?)
}

/// A handler with some error handling.
///
/// Depending on the argument, it either returns a result or an error
/// (that may be NotFound or BadRequest).
async fn arg_handler(what: String) -> Result<Response> {
// Note: This parsing could be done by typing `what` as usize in the
// function signature. This is just an example for mapping an error.
let n: usize = what.parse().map_err(|_| MyError::NotFound)?;
let w = match n {
0 => return Err(MyError::BadRequest),
1 => "one",
2 | 3 | 5 | 7 | 11 | 13 => "prime",
4 | 6 | 8 | 10 | 12 | 14 => "even",
9 | 15 => "odd",
_ => return Err(MyError::BadRequest),
};
Ok(Builder::new()
.html(|o| templates::page(o, &[("first", 0), (w, n)]))?)
}

/// This method can be used as a "template tag", i.e. a method that
Expand All @@ -59,41 +86,87 @@ fn footer(out: &mut dyn Write) -> io::Result<()> {
/// Handler for static files.
/// Create a response from the file data with a correct content type
/// and a far expires header (or a 404 if the file does not exist).
async fn static_file(name: String) -> Result<impl Reply, Rejection> {
async fn static_file(name: String) -> Result<impl Reply> {
if let Some(data) = StaticFile::get(&name) {
let _far_expires = SystemTime::now() + FAR;
Ok(Response::builder()
Ok(Builder::new()
.status(StatusCode::OK)
.header("content-type", data.mime.as_ref())
// TODO .header("expires", _far_expires)
.body(data.content))
} else {
println!("Static file {} not found", name);
Err(warp::reject::not_found())
Err(MyError::NotFound)
}
}

/// A duration to add to current time for a far expires header.
static FAR: Duration = Duration::from_secs(180 * 24 * 60 * 60);

/// Create custom error pages.
/// Convert some rejections to MyError
///
/// This enables "nice" error responses.
async fn customize_error(err: Rejection) -> Result<impl Reply, Rejection> {
if err.is_not_found() {
eprintln!("Got a 404: {:?}", err);
// We have a custom 404 page!
Response::builder().status(StatusCode::NOT_FOUND).html(|o| {
templates::error(
o,
StatusCode::NOT_FOUND,
"The resource you requested could not be located.",
)
})
Ok(MyError::NotFound)
} else {
let code = StatusCode::INTERNAL_SERVER_ERROR; // FIXME
eprintln!("Got a {}: {:?}", code.as_u16(), err);
Response::builder()
.status(code)
.html(|o| templates::error(o, code, "Something went wrong."))
// Could identify some other errors and make nice messages here
// but warp makes that rather hard, so lets just keep the rejection here.
// that way we at least get the correct status code.
Err(err)
}
}

#[derive(Debug)]
enum MyError {
NotFound,
BadRequest,
InternalError,
}

impl std::error::Error for MyError {}

impl warp::reject::Reject for MyError {}

impl Reply for MyError {
fn into_response(self) -> Response {
match self {
MyError::NotFound => {
wrap(Builder::new().status(StatusCode::NOT_FOUND).html(|o| {
templates::error(
o,
StatusCode::NOT_FOUND,
"The resource you requested could not be located.",
)
}))
}
MyError::BadRequest => {
let code = StatusCode::BAD_REQUEST;
wrap(
Builder::new().status(code).html(|o| {
templates::error(o, code, "I won't do that.")
}),
)
}
MyError::InternalError => {
let code = StatusCode::INTERNAL_SERVER_ERROR;
wrap(Builder::new().status(code).html(|o| {
templates::error(o, code, "Something went wrong.")
}))
}
}
}
}

impl std::fmt::Display for MyError {
fn fmt(&self, out: &mut std::fmt::Formatter) -> std::fmt::Result {
out.write_str("Some error")
}
}

impl From<templates::RenderError> for MyError {
fn from(err: templates::RenderError) -> MyError {
log::error!("Failed to render: {:?}", err);
MyError::InternalError
}
}

Expand Down
8 changes: 4 additions & 4 deletions examples/warp03/templates/page.rs.html
Expand Up @@ -14,16 +14,16 @@
<main>
<h1>Example</h1>

<p>This is a simple sample page,
to be served with warp.
It contains an image of a squirrel and not much more.</p>

<figure>
<img src="/static/@squirrel_jpg.name" alt="Squirrel!"
width="240" height="160"/>
<figcaption>A squirrel</figcaption>
</figure>

<p>This is a simple sample page,
to be served with warp.
It contains an image of a squirrel and not much more.</p>

@for (order, n) in paras {
<p>This is a @order paragraph, with @n repeats.
@for _ in 1..=*n {
Expand Down
57 changes: 50 additions & 7 deletions src/templates/utils_warp03.rs
@@ -1,7 +1,8 @@
use mime::TEXT_HTML_UTF_8;
use std::error::Error;
use std::io;
use warp::http::{header::CONTENT_TYPE, response::Builder};
use warp::{reject::custom, reject::Reject, reply::Response, Rejection};
use warp::{reject::Reject, reply::Response, Reply};

/// Extension trait for [`response::Builder`] to simplify template rendering.
///
Expand Down Expand Up @@ -47,29 +48,71 @@ pub trait RenderRucte {
/// Render a template on the response builder.
///
/// This is the main function of the trait. Please see the trait documentation.
fn html<F>(self, f: F) -> Result<Response, Rejection>
fn html<F>(self, f: F) -> Result<Response, RenderError>
where
F: FnOnce(&mut Vec<u8>) -> io::Result<()>;
}

impl RenderRucte for Builder {
fn html<F>(self, f: F) -> Result<Response, Rejection>
fn html<F>(self, f: F) -> Result<Response, RenderError>
where
F: FnOnce(&mut Vec<u8>) -> io::Result<()>,
{
let mut buf = Vec::new();
f(&mut buf).map_err(RenderError::Write).map_err(custom)?;
f(&mut buf).map_err(RenderError::write)?;
self.header(CONTENT_TYPE, TEXT_HTML_UTF_8.as_ref())
.body(buf.into())
.map_err(RenderError::Build)
.map_err(custom)
.map_err(RenderError::build)
}
}

/// Error type for [`RenderRucte::html`].
///
/// This type implements [`Error`] for common Rust error handling, but
/// also both [`Reply`] and [`Reject`] to facilitate use in warp filters
/// and handlers.
#[derive(Debug)]
pub struct RenderError {
im: RenderErrorImpl,
}
impl RenderError {
fn build(e: warp::http::Error) -> Self {
RenderError { im: RenderErrorImpl::Build(e) }
}
fn write(e: std::io::Error) -> Self {
RenderError { im: RenderErrorImpl::Write(e) }
}
}

// make variants private
#[derive(Debug)]
enum RenderError {
enum RenderErrorImpl {
Write(std::io::Error),
Build(warp::http::Error),
}

impl Error for RenderError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match &self.im {
RenderErrorImpl::Write(e) => Some(e),
RenderErrorImpl::Build(e) => Some(e),
}
}
}

impl std::fmt::Display for RenderError {
fn fmt(&self, out: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.im {
RenderErrorImpl::Write(_) => "Failed to write template",
RenderErrorImpl::Build(_) => "Failed to build response",
}.fmt(out)
}
}

impl Reject for RenderError {}

impl Reply for RenderError {
fn into_response(self) -> Response {
Response::new(self.to_string().into())
}
}