Skip to content

Commit 7da1b28

Browse files
committed
implement default 404 error page
... and update routing logic to serve it; remove old 404 handling code; add tests for new behavior
1 parent 62e826c commit 7da1b28

File tree

7 files changed

+111
-41
lines changed

7 files changed

+111
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- Fix a bug with zero values being displayed with a non-zero height in stacked bar charts.
55
- Updated dependencies, including the embedded SQLite database.
66
- Release binaries are now dynamically linked again, but use GLIBC 2.28 ([released in 2018](https://sourceware.org/glibc/wiki/Glibc%20Timeline)), with is compatible with older linux distributions.
7+
- When an user requests a page that does not exist (and the site owner did not provide a custom 404.sql file), we now serve a nice visual 404 web page instead of the ugly textual message and the verbose log messages we used to have.
78

89
## v0.35.1
910
- improve color palette for charts

_default_404.sql

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
SELECT
2+
'shell' as component,
3+
'Page Not Found' as title,
4+
'error-404' as body_class,
5+
'/' as link;
6+
7+
SELECT
8+
'empty_state' as component,
9+
'Page Not Found' as title,
10+
'404' as header,
11+
'The page you were looking for does not exist.' as description_md,
12+
'Go to Homepage' as link_text,
13+
'home' as link_icon,
14+
'/' as link;
15+
16+
select
17+
'text' as component,
18+
'
19+
> **Routing Debug Info**
20+
> When a URL is requested, SQLPage looks for matching files in this order:
21+
> 1. **Exact filename match** (e.g. `page.html` for `/page.html`)
22+
> 2. **For paths ending with `/`**:
23+
> - Looks for `index.sql` in that directory (e.g. `/dir/` → `dir/index.sql`)
24+
> 3. **For paths without extensions**:
25+
> - First tries adding `.sql` extension (e.g. `/dir/page` → `dir/page.sql`)
26+
> - If not found, redirects to add trailing `/` (e.g. `/dir` → `/dir/`)
27+
> 4. **If no matches found**:
28+
> - Searches for `404.sql` in current and parent directories (e.g. `dir/x/y/` could use `dir/404.sql`)
29+
>
30+
> Try creating one of these files to handle this route.
31+
' as contents_md;

src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ impl AppState {
120120
PathBuf::from("index.sql"),
121121
ParsedSqlFile::new(&db, include_str!("../index.sql"), Path::new("index.sql")),
122122
);
123+
sql_file_cache.add_static(
124+
PathBuf::from("_default_404.sql"),
125+
ParsedSqlFile::new(
126+
&db,
127+
include_str!("../_default_404.sql"),
128+
Path::new("_default_404.sql"),
129+
),
130+
);
123131

124132
let oidc_state = crate::webserver::oidc::initialize_oidc_state(config).await?;
125133

src/webserver/http.rs

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -357,29 +357,6 @@ fn strip_site_prefix<'a>(path: &'a str, state: &AppState) -> &'a str {
357357
path.strip_prefix(&state.config.site_prefix).unwrap_or(path)
358358
}
359359

360-
/// Serves a fallback 404 error page (when no 404 handler is found)
361-
fn serve_not_found(service_request: &mut ServiceRequest) -> actix_web::Error {
362-
let app_state: &web::Data<AppState> = service_request.app_data().expect("app_state");
363-
let source_err = anyhow::anyhow!(ErrorWithStatus {
364-
status: StatusCode::NOT_FOUND
365-
});
366-
let path = service_request.path();
367-
let mut message = format!("{path} does not exist.");
368-
if !app_state.config.environment.is_prod() {
369-
message.push_str("\n\nRouting Debug Info:\n\
370-
- SQLPage first looks for an exact match of your file (e.g. 'page.sql')\n\
371-
- For paths without extensions that end in '/', SQLPage looks for 'index.sql' in that directory (e.g. '/dir/' loads 'dir/index.sql')\n\
372-
- For paths without extensions that don't end in '/', SQLPage tries:\n\
373-
1. Looking for the path with '.sql' extension (e.g. '/dir/page' loads 'dir/page.sql')\n\
374-
2. If not found, redirects by adding a trailing '/' (e.g. '/dir' redirects to '/dir/')\n\
375-
- When no file is found, SQLPage looks for '404.sql' in the current and parent directories (e.g. 'dir/x/y' may load 'dir/404.sql')\n\
376-
\nTry creating one of these files to handle this route.");
377-
}
378-
379-
let err = source_err.context(message);
380-
anyhow_err_to_actix(err, app_state.config.environment)
381-
}
382-
383360
pub async fn main_handler(
384361
mut service_request: ServiceRequest,
385362
) -> actix_web::Result<ServiceResponse> {
@@ -399,7 +376,13 @@ pub async fn main_handler(
399376
}
400377
};
401378
match routing_action {
402-
NotFound => Err(serve_not_found(&mut service_request)),
379+
NotFound => {
380+
let mut response =
381+
process_sql_request(&mut service_request, PathBuf::from("_default_404.sql"))
382+
.await?;
383+
*response.status_mut() = StatusCode::NOT_FOUND;
384+
Ok(response)
385+
}
403386
Execute(path) => process_sql_request(&mut service_request, path).await,
404387
CustomNotFound(path) => {
405388
// Currently, we do not set a 404 status when the user provides a fallback 404.sql file.

tests/basic/mod.rs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,6 @@ async fn test_access_config_forbidden() {
3030
);
3131
}
3232

33-
#[actix_web::test]
34-
async fn test_404() {
35-
for f in [
36-
"/does_not_exist.sql",
37-
"/does_not_exist.html",
38-
"/does_not_exist/",
39-
] {
40-
let resp_result = req_path(f).await;
41-
let resp = resp_result.unwrap_err().error_response();
42-
assert_eq!(resp.status(), http::StatusCode::NOT_FOUND, "{f} isnt 404");
43-
}
44-
}
45-
4633
#[actix_web::test]
4734
async fn test_static_files() {
4835
let resp = req_path("/tests/it_works.txt").await.unwrap();

tests/core/mod.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,12 @@ async fn test_routing_with_prefix() {
104104

105105
let resp = req_path_with_app_data("/prefix/nonexistent.sql", app_data.clone())
106106
.await
107-
.expect_err("Expected 404 error")
108-
.to_string();
107+
.expect("should handle 404");
108+
let body = test::read_body(resp).await;
109+
let body_str = String::from_utf8(body.to_vec()).unwrap();
109110
assert!(
110-
resp.contains("404"),
111-
"Response should contain \"404\", but got:\n{resp}"
111+
body_str.contains("404"),
112+
"Response should contain \"404\", but got:\n{body_str}"
112113
);
113114

114115
let resp = req_path_with_app_data("/prefix/sqlpage/migrations/0001_init.sql", app_data.clone())

tests/errors/mod.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,62 @@ async fn test_404_fallback() {
4141
assert!(!body.contains("error"));
4242
}
4343
}
44+
45+
#[actix_web::test]
46+
async fn test_default_404() {
47+
for f in [
48+
"/i-do-not-exist.html",
49+
"/i-do-not-exist.sql",
50+
"/i-do-not-exist/",
51+
] {
52+
let resp_result = req_path(f).await;
53+
let resp = resp_result.unwrap();
54+
assert_eq!(
55+
resp.status(),
56+
http::StatusCode::NOT_FOUND,
57+
"{f} should return 404"
58+
);
59+
60+
let body = test::read_body(resp).await;
61+
assert!(body.starts_with(b"<!DOCTYPE html>"));
62+
let body = String::from_utf8(body.to_vec()).unwrap();
63+
let msg = "The page you were looking for does not exist";
64+
assert!(
65+
body.contains(msg),
66+
"{f} should contain '{msg}' but got:\n{body}"
67+
);
68+
assert!(!body.contains("error"));
69+
}
70+
}
71+
72+
#[actix_web::test]
73+
async fn test_default_404_with_redirect() {
74+
let resp_result = req_path("/i-do-not-exist").await;
75+
let resp = resp_result.unwrap();
76+
assert_eq!(
77+
resp.status(),
78+
http::StatusCode::MOVED_PERMANENTLY,
79+
"/i-do-not-exist should return 301"
80+
);
81+
82+
let location = resp.headers().get(http::header::LOCATION).unwrap();
83+
assert_eq!(location, "/i-do-not-exist/");
84+
85+
let resp_result = req_path("/i-do-not-exist/").await;
86+
let resp = resp_result.unwrap();
87+
assert_eq!(
88+
resp.status(),
89+
http::StatusCode::NOT_FOUND,
90+
"/i-do-not-exist/ should return 404"
91+
);
92+
93+
let body = test::read_body(resp).await;
94+
assert!(body.starts_with(b"<!DOCTYPE html>"));
95+
let body = String::from_utf8(body.to_vec()).unwrap();
96+
let msg = "The page you were looking for does not exist";
97+
assert!(
98+
body.contains(msg),
99+
"/i-do-not-exist/ should contain '{msg}' but got:\n{body}"
100+
);
101+
assert!(!body.contains("error"));
102+
}

0 commit comments

Comments
 (0)