Skip to content

Commit bb722d9

Browse files
committed
move http error handling to a dedicated module
1 parent 50bad4e commit bb722d9

File tree

3 files changed

+114
-98
lines changed

3 files changed

+114
-98
lines changed

src/webserver/error.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//! HTTP error handling
2+
3+
use std::path::PathBuf;
4+
5+
use crate::webserver::ErrorWithStatus;
6+
use crate::AppState;
7+
use actix_web::error::UrlencodedError;
8+
use actix_web::http::{header, StatusCode};
9+
use actix_web::{HttpRequest, HttpResponse};
10+
use actix_web::{HttpResponseBuilder, ResponseError};
11+
12+
fn anyhow_err_to_actix_resp(e: &anyhow::Error, state: &AppState) -> HttpResponse {
13+
let mut resp = HttpResponseBuilder::new(StatusCode::INTERNAL_SERVER_ERROR);
14+
let mut body = "Sorry, but we were not able to process your request.\n\n".to_owned();
15+
let env = state.config.environment;
16+
if env.is_prod() {
17+
body.push_str("Contact the administrator for more information. A detailed error message has been logged.");
18+
log::error!("{e:#}");
19+
} else {
20+
use std::fmt::Write;
21+
write!(
22+
body,
23+
"Below are detailed debugging information which may contain sensitive data. \n\
24+
Set environment to \"production\" in the configuration file to hide this information. \n\n\
25+
{e:?}"
26+
)
27+
.unwrap();
28+
}
29+
resp.insert_header((
30+
header::CONTENT_TYPE,
31+
header::HeaderValue::from_static("text/plain; charset=utf-8"),
32+
));
33+
34+
if let Some(status_err @ &ErrorWithStatus { .. }) = e.downcast_ref() {
35+
status_err
36+
.error_response()
37+
.set_body(actix_web::body::BoxBody::new(body))
38+
} else if let Some(sqlx::Error::PoolTimedOut) = e.downcast_ref() {
39+
use rand::Rng;
40+
resp.status(StatusCode::TOO_MANY_REQUESTS)
41+
.insert_header((
42+
header::RETRY_AFTER,
43+
header::HeaderValue::from(rand::rng().random_range(1..=15)),
44+
))
45+
.body("The database is currently too busy to handle your request. Please try again later.\n\n".to_owned() + &body)
46+
} else {
47+
resp.body(body)
48+
}
49+
}
50+
51+
pub(super) fn send_anyhow_error(
52+
e: &anyhow::Error,
53+
resp_send: tokio::sync::oneshot::Sender<HttpResponse>,
54+
state: &AppState,
55+
) {
56+
log::error!("An error occurred before starting to send the response body: {e:#}");
57+
resp_send
58+
.send(anyhow_err_to_actix_resp(e, state))
59+
.unwrap_or_else(|_| log::error!("could not send headers"));
60+
}
61+
62+
pub(super) fn anyhow_err_to_actix(e: anyhow::Error, state: &AppState) -> actix_web::Error {
63+
log::error!("{e:#}");
64+
let resp = anyhow_err_to_actix_resp(&e, state);
65+
actix_web::error::InternalError::from_response(e, resp).into()
66+
}
67+
68+
pub(super) fn handle_form_error(
69+
decode_err: UrlencodedError,
70+
_req: &HttpRequest,
71+
) -> actix_web::Error {
72+
match decode_err {
73+
actix_web::error::UrlencodedError::Overflow { size, limit } => {
74+
actix_web::error::ErrorPayloadTooLarge(
75+
format!(
76+
"The submitted form data size ({size} bytes) exceeds the maximum allowed upload size ({limit} bytes). \
77+
You can increase this limit by setting max_uploaded_file_size in the configuration file.",
78+
),
79+
)
80+
}
81+
_ => actix_web::Error::from(decode_err),
82+
}
83+
}
84+
85+
pub(super) fn bind_error(e: std::io::Error, listen_on: std::net::SocketAddr) -> anyhow::Error {
86+
let (ip, port) = (listen_on.ip(), listen_on.port());
87+
// Let's try to give a more helpful error message in common cases
88+
let ctx = match e.kind() {
89+
std::io::ErrorKind::AddrInUse => format!(
90+
"Another program is already using port {port} (maybe {} ?). \
91+
You can either stop that program or change the port in the configuration file.",
92+
if port == 80 || port == 443 {
93+
"Apache or Nginx"
94+
} else {
95+
"another instance of SQLPage"
96+
},
97+
),
98+
std::io::ErrorKind::PermissionDenied => format!(
99+
"You do not have permission to bind to {ip} on port {port}. \
100+
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}`, \
101+
or change the port in the configuration file.",
102+
executable_path = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("sqlpage.bin")).display(),
103+
),
104+
std::io::ErrorKind::AddrNotAvailable => format!(
105+
"The IP address {ip} does not exist on this computer. \
106+
You can change the value of listen_on in the configuration file.",
107+
),
108+
_ => format!("Unable to bind to {ip} on port {port}"),
109+
};
110+
anyhow::anyhow!(e).context(ctx)
111+
}

src/webserver/http.rs

Lines changed: 2 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ use actix_web::web::PayloadConfig;
1717
use actix_web::{
1818
dev::ServiceResponse, middleware, middleware::Logger, web, App, HttpResponse, HttpServer,
1919
};
20-
use actix_web::{HttpResponseBuilder, ResponseError};
2120

21+
use super::error::{anyhow_err_to_actix, bind_error, send_anyhow_error};
2222
use super::http_client::make_http_client;
2323
use super::https::make_auto_rustls_config;
2424
use super::oidc::OidcMiddleware;
@@ -213,62 +213,6 @@ async fn render_sql(
213213
resp_recv.await.map_err(ErrorInternalServerError)
214214
}
215215

216-
fn anyhow_err_to_actix_resp(e: &anyhow::Error, state: &AppState) -> HttpResponse {
217-
let mut resp = HttpResponseBuilder::new(StatusCode::INTERNAL_SERVER_ERROR);
218-
let mut body = "Sorry, but we were not able to process your request.\n\n".to_owned();
219-
let env = state.config.environment;
220-
if env.is_prod() {
221-
body.push_str("Contact the administrator for more information. A detailed error message has been logged.");
222-
log::error!("{e:#}");
223-
} else {
224-
use std::fmt::Write;
225-
write!(
226-
body,
227-
"Below are detailed debugging information which may contain sensitive data. \n\
228-
Set environment to \"production\" in the configuration file to hide this information. \n\n\
229-
{e:?}"
230-
)
231-
.unwrap();
232-
}
233-
resp.insert_header((
234-
header::CONTENT_TYPE,
235-
header::HeaderValue::from_static("text/plain; charset=utf-8"),
236-
));
237-
238-
if let Some(status_err @ &ErrorWithStatus { .. }) = e.downcast_ref() {
239-
status_err
240-
.error_response()
241-
.set_body(actix_web::body::BoxBody::new(body))
242-
} else if let Some(sqlx::Error::PoolTimedOut) = e.downcast_ref() {
243-
use rand::Rng;
244-
resp.status(StatusCode::TOO_MANY_REQUESTS)
245-
.insert_header((
246-
header::RETRY_AFTER,
247-
header::HeaderValue::from(rand::rng().random_range(1..=15)),
248-
))
249-
.body("The database is currently too busy to handle your request. Please try again later.\n\n".to_owned() + &body)
250-
} else {
251-
resp.body(body)
252-
}
253-
}
254-
255-
fn send_anyhow_error(
256-
e: &anyhow::Error,
257-
resp_send: tokio::sync::oneshot::Sender<HttpResponse>,
258-
state: &AppState,
259-
) {
260-
log::error!("An error occurred before starting to send the response body: {e:#}");
261-
resp_send
262-
.send(anyhow_err_to_actix_resp(e, state))
263-
.unwrap_or_else(|_| log::error!("could not send headers"));
264-
}
265-
266-
fn anyhow_err_to_actix(e: anyhow::Error, state: &AppState) -> actix_web::Error {
267-
log::error!("{e:#}");
268-
let resp = anyhow_err_to_actix_resp(&e, state);
269-
actix_web::error::InternalError::from_response(e, resp).into()
270-
}
271-
272216
#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Clone)]
273217
#[serde(untagged)]
274218
pub enum SingleOrVec {
@@ -501,19 +445,7 @@ pub fn create_app(
501445
pub fn form_config(app_state: &web::Data<AppState>) -> web::FormConfig {
502446
web::FormConfig::default()
503447
.limit(app_state.config.max_uploaded_file_size)
504-
.error_handler(|decode_err, _req| {
505-
match decode_err {
506-
actix_web::error::UrlencodedError::Overflow { size, limit } => {
507-
actix_web::error::ErrorPayloadTooLarge(
508-
format!(
509-
"The submitted form data size ({size} bytes) exceeds the maximum allowed upload size ({limit} bytes). \
510-
You can increase this limit by setting max_uploaded_file_size in the configuration file.",
511-
),
512-
)
513-
}
514-
_ => actix_web::Error::from(decode_err),
515-
}
516-
})
448+
.error_handler(super::error::handle_form_error)
517449
}
518450

519451
#[must_use]
@@ -619,34 +551,6 @@ fn log_welcome_message(config: &AppConfig) {
619551
);
620552
}
621553

622-
fn bind_error(e: std::io::Error, listen_on: std::net::SocketAddr) -> anyhow::Error {
623-
let (ip, port) = (listen_on.ip(), listen_on.port());
624-
// Let's try to give a more helpful error message in common cases
625-
let ctx = match e.kind() {
626-
std::io::ErrorKind::AddrInUse => format!(
627-
"Another program is already using port {port} (maybe {} ?). \
628-
You can either stop that program or change the port in the configuration file.",
629-
if port == 80 || port == 443 {
630-
"Apache or Nginx"
631-
} else {
632-
"another instance of SQLPage"
633-
},
634-
),
635-
std::io::ErrorKind::PermissionDenied => format!(
636-
"You do not have permission to bind to {ip} on port {port}. \
637-
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}`, \
638-
or change the port in the configuration file.",
639-
executable_path = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("sqlpage.bin")).display(),
640-
),
641-
std::io::ErrorKind::AddrNotAvailable => format!(
642-
"The IP address {ip} does not exist on this computer. \
643-
You can change the value of listen_on in the configuration file.",
644-
),
645-
_ => format!("Unable to bind to {ip} on port {port}"),
646-
};
647-
anyhow::anyhow!(e).context(ctx)
648-
}
649-
650554
#[cfg(target_family = "unix")]
651555
fn bind_unix_socket_err(e: std::io::Error, unix_socket: &std::path::Path) -> anyhow::Error {
652556
let ctx = if e.kind() == std::io::ErrorKind::PermissionDenied {

src/webserver/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
3232
pub mod content_security_policy;
3333
pub mod database;
34+
mod error;
3435
pub mod error_with_status;
3536
pub mod http;
3637
pub mod http_client;

0 commit comments

Comments
 (0)