move http error handling to a dedicated module
This commit is contained in:
@@ -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<HttpResponse>,
|
||||
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)
|
||||
}
|
||||
+2
-98
@@ -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<HttpResponse>,
|
||||
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<AppState>) -> 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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user