Skip to content

Commit

Permalink
new configuration option: environment
Browse files Browse the repository at this point in the history
allows hiding errors from users

closes #145

Thanks to @demogit-code
  • Loading branch information
lovasoa committed Dec 3, 2023
1 parent a6bdec7 commit 51f5840
Show file tree
Hide file tree
Showing 8 changed files with 63 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- change breakpoints in the [hero](https://sql.ophir.dev/documentation.sql?component=hero#component) component to make it more responsive on middle-sized screens such as tablets or small laptops. This avoids the hero image taking up the whole screen on these devices.
- add an `image_url` row-level attribute to the [list](https://sql.ophir.dev/documentation.sql?component=list#component) component to display small images in lists.
- Fix bad contrast in links in custom page footers.
- Add a new [configuration option](./configuration.md): `environment`. This allows you to set the environment in which SQLPage is running. It can be either `development` or `production`. In `production` mode, SQLPage will hide error messages and stack traces from the user, and will cache sql files in memory to avoid reloading them from disk when under heavy load.

## 0.17.0

Expand Down
1 change: 1 addition & 0 deletions configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Here are the available configuration options and their default values:
| `https_certificate_email` | contact@<https_domain> | The email address to use when requesting a certificate. |
| `https_certificate_cache_dir` | ./sqlpage/https | A writeable directory where to cache the certificates, so that SQLPage can serve https traffic immediately when it restarts. |
| `https_acme_directory_url` | https://acme-v02.api.letsencrypt.org/directory | The URL of the ACME directory to use when requesting a certificate. |
| `environment` | development | The environment in which SQLPage is running. Can be either `development` or `production`. In `production` mode, SQLPage will hide error messages and stack traces from the user, and will cache sql files in memory to avoid reloading them from disk. |

Multiple configuration file formats are supported:
you can use a [`.json5`](https://json5.org/) file, a [`.toml`](https://toml.io/) file, or a [`.yaml`](https://en.wikipedia.org/wiki/YAML#Syntax) file.
Expand Down
3 changes: 3 additions & 0 deletions sqlpage/templates/error.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@
{{~/each~}}
</details>
{{/if}}
{{#if note}}
<p class="fs-5 mt-1 p-1 my-1">{{note}}</p>
{{/if}}
</div>
18 changes: 18 additions & 0 deletions src/app_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ pub struct AppConfig {
/// URL to the ACME directory. Defaults to the Let's Encrypt production directory.
#[serde(default = "default_https_acme_directory_url")]
pub https_acme_directory_url: String,

/// Whether SQLPage is running in development or production mode. This is used to determine
/// whether to show error messages to the user.
#[serde(default)]
pub environment: DevOrProd,
}

impl AppConfig {
Expand Down Expand Up @@ -181,6 +186,19 @@ fn default_https_acme_directory_url() -> String {
"https://acme-v02.api.letsencrypt.org/directory".to_string()
}

#[derive(Debug, Deserialize, PartialEq, Clone, Copy, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum DevOrProd {
#[default]
Development,
Production,
}
impl DevOrProd {
pub(crate) fn is_prod(self) -> bool {
self == DevOrProd::Production
}
}

#[cfg(test)]
pub mod tests {
use super::AppConfig;
Expand Down
6 changes: 4 additions & 2 deletions src/file_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ use std::sync::atomic::{
use std::sync::Arc;
use std::time::SystemTime;

const MAX_STALE_CACHE_MS: u64 = 100;
/// The maximum time in milliseconds that a file can be cached before its freshness is checked
/// (in production mode)
const MAX_STALE_CACHE_MS: u64 = 150;

#[derive(Default)]
struct Cached<T> {
Expand Down Expand Up @@ -95,7 +97,7 @@ impl<T: AsyncFromStrWithState> FileCache<T> {

pub async fn get(&self, app_state: &AppState, path: &PathBuf) -> anyhow::Result<Arc<T>> {
if let Some(cached) = self.cache.get(path) {
if !cached.needs_check() {
if app_state.config.environment.is_prod() && !cached.needs_check() {
log::trace!("Cache answer without filesystem lookup for {:?}", path);
return Ok(Arc::clone(&cached.content));
}
Expand Down
23 changes: 16 additions & 7 deletions src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ impl<W: std::io::Write> HeaderContext<W> {
}

pub async fn handle_error(self, err: anyhow::Error) -> anyhow::Result<PageContext<W>> {
if self.app_state.config.environment.is_prod() {
return Err(err);
}
log::debug!("Handling header error: {err}");
let data = json!({
"component": "error",
Expand Down Expand Up @@ -418,14 +421,20 @@ impl<W: std::io::Write> RenderContext<W> {
/// Handles the rendering of an error.
/// Returns whether the error is irrecoverable and the rendering must stop
pub async fn handle_error(&mut self, error: &anyhow::Error) -> anyhow::Result<()> {
log::warn!("SQL error: {:?}", error);
log::error!("SQL error: {:?}", error);
self.close_component()?;
let description = error.to_string();
let data = json!({
"query_number": self.current_statement,
"description": description,
"backtrace": get_backtrace(error)
});
let data = if self.app_state.config.environment.is_prod() {
json!({
"description": format!("Please contact the administrator for more information. The error has been logged."),
})
} else {
json!({
"query_number": self.current_statement,
"description": error.to_string(),
"backtrace": get_backtrace(error),
"note": "You can hide error messages like this one from your users by setting the 'environment' configuration option to 'production'."
})
};
let saved_component = self.open_component_with_data("error", &data).await?;
self.close_component()?;
self.current_component = saved_component;
Expand Down
5 changes: 4 additions & 1 deletion src/webserver/error_with_status.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use actix_web::{http::{StatusCode, header::ContentType}, ResponseError};
use actix_web::{
http::{header::ContentType, StatusCode},
ResponseError,
};

#[derive(Debug, PartialEq)]
pub struct ErrorWithStatus {
Expand Down
22 changes: 16 additions & 6 deletions src/webserver/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::render::{HeaderContext, PageContext, RenderContext};
use crate::webserver::database::{execute_queries::stream_query_results, DbItem};
use crate::webserver::http_request_info::extract_request_info;
use crate::webserver::ErrorWithStatus;
use crate::{AppConfig, AppState, ParsedSqlFile};
use crate::{app_config, AppConfig, AppState, ParsedSqlFile};
use actix_web::dev::{fn_service, ServiceFactory, ServiceRequest};
use actix_web::error::ErrorInternalServerError;
use actix_web::http::header::{ContentType, Header, HttpDate, IfModifiedSince, LastModified};
Expand Down Expand Up @@ -239,18 +239,28 @@ async fn render_sql(
.unwrap_or_else(|e| log::error!("could not send headers {e:?}"));
}
Err(err) => {
send_anyhow_error(&err, resp_send);
send_anyhow_error(&err, resp_send, app_state.config.environment);
}
}
});
resp_recv.await.map_err(ErrorInternalServerError)
}

fn send_anyhow_error(e: &anyhow::Error, resp_send: tokio::sync::oneshot::Sender<HttpResponse>) {
fn send_anyhow_error(
e: &anyhow::Error,
resp_send: tokio::sync::oneshot::Sender<HttpResponse>,
env: app_config::DevOrProd,
) {
log::error!("An error occurred before starting to send the response body: {e:#}");
let mut resp = HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR).set_body(BoxBody::new(
format!("Sorry, but we were not able to process your request. \n\nError:\n\n {e:?}"),
));
let mut resp = HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR);
let mut body = "Sorry, but we were not able to process your request. \n\n".to_owned();
if env.is_prod() {
body.push_str("Contact the administrator for more information. A detailed error message has been logged.");
} else {
use std::fmt::Write;
write!(body, "{e:#}").unwrap();
}
resp = resp.set_body(BoxBody::new(body));
resp.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("text/plain"),
Expand Down

0 comments on commit 51f5840

Please sign in to comment.