From bb722d9e71f672182e07a7430dd627339e42d85d Mon Sep 17 00:00:00 2001 From: lovasoa Date: Sat, 30 Aug 2025 14:48:47 +0200 Subject: [PATCH] move http error handling to a dedicated module --- src/webserver/error.rs | 111 +++++++++++++++++++++++++++++++++++++++++ src/webserver/http.rs | 100 +------------------------------------ src/webserver/mod.rs | 1 + 3 files changed, 114 insertions(+), 98 deletions(-) create mode 100644 src/webserver/error.rs diff --git a/src/webserver/error.rs b/src/webserver/error.rs new file mode 100644 index 00000000..19a03520 --- /dev/null +++ b/src/webserver/error.rs @@ -0,0 +1,111 @@ +//! HTTP error handling + +use std::path::PathBuf; + +use crate::webserver::ErrorWithStatus; +use crate::AppState; +use actix_web::error::UrlencodedError; +use actix_web::http::{header, StatusCode}; +use actix_web::{HttpRequest, HttpResponse}; +use actix_web::{HttpResponseBuilder, ResponseError}; + +fn anyhow_err_to_actix_resp(e: &anyhow::Error, state: &AppState) -> HttpResponse { + let mut resp = HttpResponseBuilder::new(StatusCode::INTERNAL_SERVER_ERROR); + let mut body = "Sorry, but we were not able to process your request.\n\n".to_owned(); + let env = state.config.environment; + if env.is_prod() { + body.push_str("Contact the administrator for more information. A detailed error message has been logged."); + log::error!("{e:#}"); + } else { + use std::fmt::Write; + write!( + body, + "Below are detailed debugging information which may contain sensitive data. \n\ + Set environment to \"production\" in the configuration file to hide this information. \n\n\ + {e:?}" + ) + .unwrap(); + } + resp.insert_header(( + header::CONTENT_TYPE, + header::HeaderValue::from_static("text/plain; charset=utf-8"), + )); + + if let Some(status_err @ &ErrorWithStatus { .. }) = e.downcast_ref() { + status_err + .error_response() + .set_body(actix_web::body::BoxBody::new(body)) + } else if let Some(sqlx::Error::PoolTimedOut) = e.downcast_ref() { + use rand::Rng; + resp.status(StatusCode::TOO_MANY_REQUESTS) + .insert_header(( + header::RETRY_AFTER, + header::HeaderValue::from(rand::rng().random_range(1..=15)), + )) + .body("The database is currently too busy to handle your request. Please try again later.\n\n".to_owned() + &body) + } else { + resp.body(body) + } +} + +pub(super) fn send_anyhow_error( + e: &anyhow::Error, + resp_send: tokio::sync::oneshot::Sender, + state: &AppState, +) { + log::error!("An error occurred before starting to send the response body: {e:#}"); + resp_send + .send(anyhow_err_to_actix_resp(e, state)) + .unwrap_or_else(|_| log::error!("could not send headers")); +} + +pub(super) fn anyhow_err_to_actix(e: anyhow::Error, state: &AppState) -> actix_web::Error { + log::error!("{e:#}"); + let resp = anyhow_err_to_actix_resp(&e, state); + actix_web::error::InternalError::from_response(e, resp).into() +} + +pub(super) fn handle_form_error( + decode_err: UrlencodedError, + _req: &HttpRequest, +) -> actix_web::Error { + match decode_err { + actix_web::error::UrlencodedError::Overflow { size, limit } => { + actix_web::error::ErrorPayloadTooLarge( + format!( + "The submitted form data size ({size} bytes) exceeds the maximum allowed upload size ({limit} bytes). \ + You can increase this limit by setting max_uploaded_file_size in the configuration file.", + ), + ) + } + _ => actix_web::Error::from(decode_err), + } +} + +pub(super) fn bind_error(e: std::io::Error, listen_on: std::net::SocketAddr) -> anyhow::Error { + let (ip, port) = (listen_on.ip(), listen_on.port()); + // Let's try to give a more helpful error message in common cases + let ctx = match e.kind() { + std::io::ErrorKind::AddrInUse => format!( + "Another program is already using port {port} (maybe {} ?). \ + You can either stop that program or change the port in the configuration file.", + if port == 80 || port == 443 { + "Apache or Nginx" + } else { + "another instance of SQLPage" + }, + ), + std::io::ErrorKind::PermissionDenied => format!( + "You do not have permission to bind to {ip} on port {port}. \ + You can either run SQLPage as root with sudo, give it the permission to bind to low ports with `sudo setcap cap_net_bind_service=+ep {executable_path}`, \ + or change the port in the configuration file.", + executable_path = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("sqlpage.bin")).display(), + ), + std::io::ErrorKind::AddrNotAvailable => format!( + "The IP address {ip} does not exist on this computer. \ + You can change the value of listen_on in the configuration file.", + ), + _ => format!("Unable to bind to {ip} on port {port}"), + }; + anyhow::anyhow!(e).context(ctx) +} diff --git a/src/webserver/http.rs b/src/webserver/http.rs index 5f595f3c..542f7fb4 100644 --- a/src/webserver/http.rs +++ b/src/webserver/http.rs @@ -17,8 +17,8 @@ use actix_web::web::PayloadConfig; use actix_web::{ dev::ServiceResponse, middleware, middleware::Logger, web, App, HttpResponse, HttpServer, }; -use actix_web::{HttpResponseBuilder, ResponseError}; +use super::error::{anyhow_err_to_actix, bind_error, send_anyhow_error}; use super::http_client::make_http_client; use super::https::make_auto_rustls_config; use super::oidc::OidcMiddleware; @@ -213,62 +213,6 @@ async fn render_sql( resp_recv.await.map_err(ErrorInternalServerError) } -fn anyhow_err_to_actix_resp(e: &anyhow::Error, state: &AppState) -> HttpResponse { - let mut resp = HttpResponseBuilder::new(StatusCode::INTERNAL_SERVER_ERROR); - let mut body = "Sorry, but we were not able to process your request.\n\n".to_owned(); - let env = state.config.environment; - if env.is_prod() { - body.push_str("Contact the administrator for more information. A detailed error message has been logged."); - log::error!("{e:#}"); - } else { - use std::fmt::Write; - write!( - body, - "Below are detailed debugging information which may contain sensitive data. \n\ - Set environment to \"production\" in the configuration file to hide this information. \n\n\ - {e:?}" - ) - .unwrap(); - } - resp.insert_header(( - header::CONTENT_TYPE, - header::HeaderValue::from_static("text/plain; charset=utf-8"), - )); - - if let Some(status_err @ &ErrorWithStatus { .. }) = e.downcast_ref() { - status_err - .error_response() - .set_body(actix_web::body::BoxBody::new(body)) - } else if let Some(sqlx::Error::PoolTimedOut) = e.downcast_ref() { - use rand::Rng; - resp.status(StatusCode::TOO_MANY_REQUESTS) - .insert_header(( - header::RETRY_AFTER, - header::HeaderValue::from(rand::rng().random_range(1..=15)), - )) - .body("The database is currently too busy to handle your request. Please try again later.\n\n".to_owned() + &body) - } else { - resp.body(body) - } -} - -fn send_anyhow_error( - e: &anyhow::Error, - resp_send: tokio::sync::oneshot::Sender, - state: &AppState, -) { - log::error!("An error occurred before starting to send the response body: {e:#}"); - resp_send - .send(anyhow_err_to_actix_resp(e, state)) - .unwrap_or_else(|_| log::error!("could not send headers")); -} - -fn anyhow_err_to_actix(e: anyhow::Error, state: &AppState) -> actix_web::Error { - log::error!("{e:#}"); - let resp = anyhow_err_to_actix_resp(&e, state); - actix_web::error::InternalError::from_response(e, resp).into() -} - #[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Clone)] #[serde(untagged)] pub enum SingleOrVec { @@ -501,19 +445,7 @@ pub fn create_app( pub fn form_config(app_state: &web::Data) -> web::FormConfig { web::FormConfig::default() .limit(app_state.config.max_uploaded_file_size) - .error_handler(|decode_err, _req| { - match decode_err { - actix_web::error::UrlencodedError::Overflow { size, limit } => { - actix_web::error::ErrorPayloadTooLarge( - format!( - "The submitted form data size ({size} bytes) exceeds the maximum allowed upload size ({limit} bytes). \ - You can increase this limit by setting max_uploaded_file_size in the configuration file.", - ), - ) - } - _ => actix_web::Error::from(decode_err), - } - }) + .error_handler(super::error::handle_form_error) } #[must_use] @@ -619,34 +551,6 @@ fn log_welcome_message(config: &AppConfig) { ); } -fn bind_error(e: std::io::Error, listen_on: std::net::SocketAddr) -> anyhow::Error { - let (ip, port) = (listen_on.ip(), listen_on.port()); - // Let's try to give a more helpful error message in common cases - let ctx = match e.kind() { - std::io::ErrorKind::AddrInUse => format!( - "Another program is already using port {port} (maybe {} ?). \ - You can either stop that program or change the port in the configuration file.", - if port == 80 || port == 443 { - "Apache or Nginx" - } else { - "another instance of SQLPage" - }, - ), - std::io::ErrorKind::PermissionDenied => format!( - "You do not have permission to bind to {ip} on port {port}. \ - You can either run SQLPage as root with sudo, give it the permission to bind to low ports with `sudo setcap cap_net_bind_service=+ep {executable_path}`, \ - or change the port in the configuration file.", - executable_path = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("sqlpage.bin")).display(), - ), - std::io::ErrorKind::AddrNotAvailable => format!( - "The IP address {ip} does not exist on this computer. \ - You can change the value of listen_on in the configuration file.", - ), - _ => format!("Unable to bind to {ip} on port {port}"), - }; - anyhow::anyhow!(e).context(ctx) -} - #[cfg(target_family = "unix")] fn bind_unix_socket_err(e: std::io::Error, unix_socket: &std::path::Path) -> anyhow::Error { let ctx = if e.kind() == std::io::ErrorKind::PermissionDenied { diff --git a/src/webserver/mod.rs b/src/webserver/mod.rs index 47c33df7..9a74cc04 100644 --- a/src/webserver/mod.rs +++ b/src/webserver/mod.rs @@ -31,6 +31,7 @@ pub mod content_security_policy; pub mod database; +mod error; pub mod error_with_status; pub mod http; pub mod http_client;