From b5cf6fdfd452a21413c8f58b3a4838e62de9b63c Mon Sep 17 00:00:00 2001 From: Rasmus Kaj Date: Sat, 5 Feb 2022 21:43:49 +0100 Subject: [PATCH] Improve error handling in the warp support. --- CHANGELOG.md | 1 + examples/warp03/Cargo.toml | 1 + examples/warp03/src/main.rs | 153 ++++++++++++++++++------- examples/warp03/templates/page.rs.html | 8 +- src/templates/utils_warp03.rs | 57 +++++++-- 5 files changed, 169 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8123e15..65eaa4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/examples/warp03/Cargo.toml b/examples/warp03/Cargo.toml index 7193821..f15dfbf 100644 --- a/examples/warp03/Cargo.toml +++ b/examples/warp03/Cargo.toml @@ -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" diff --git a/examples/warp03/src/main.rs b/examples/warp03/src/main.rs index 271230c..134e02c 100644 --- a/examples/warp03/src/main.rs +++ b/examples/warp03/src/main.rs @@ -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. @@ -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 { - Response::builder().html(|o| { - templates::page(o, &[("first", 3), ("second", 7), ("third", 2)]) - }) -} +type Result = std::result::Result; -#[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 is merged, we +/// need to do this manually, with a `.map(wrap)` after the handlers +/// above. +fn wrap(result: Result) -> 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 { - Err(warp::reject::custom(SomeError)) +/// Home page handler; just render a template with some arguments. +async fn home_page() -> Result { + 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 { + // 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 @@ -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 { +async fn static_file(name: String) -> Result { 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 { 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 for MyError { + fn from(err: templates::RenderError) -> MyError { + log::error!("Failed to render: {:?}", err); + MyError::InternalError } } diff --git a/examples/warp03/templates/page.rs.html b/examples/warp03/templates/page.rs.html index c8ad365..8dce6e4 100644 --- a/examples/warp03/templates/page.rs.html +++ b/examples/warp03/templates/page.rs.html @@ -14,16 +14,16 @@

Example

-

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

-
Squirrel!
A squirrel
+

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

+ @for (order, n) in paras {

This is a @order paragraph, with @n repeats. @for _ in 1..=*n { diff --git a/src/templates/utils_warp03.rs b/src/templates/utils_warp03.rs index 4cd1341..69d138a 100644 --- a/src/templates/utils_warp03.rs +++ b/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. /// @@ -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(self, f: F) -> Result + fn html(self, f: F) -> Result where F: FnOnce(&mut Vec) -> io::Result<()>; } impl RenderRucte for Builder { - fn html(self, f: F) -> Result + fn html(self, f: F) -> Result where F: FnOnce(&mut Vec) -> 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()) + } +}