|
1 |
+ |
//! `/__errors/{status}.html` — the branded error pages, embedded in the binary.
|
|
2 |
+ |
//!
|
|
3 |
+ |
//! These three pages used to live only as files that `deploy.sh` scp'd to
|
|
4 |
+ |
//! `/opt/makenotwork/error-pages/` on every deploy, and Caddy's `handle_errors`
|
|
5 |
+ |
//! read them from disk. That coupled a per-deploy file upload to a binary that
|
|
6 |
+ |
//! versions with the copy and the brand glyphs the pages carry: the pages could
|
|
7 |
+ |
//! drift a release behind the site they front, and a forgotten `--config` run
|
|
8 |
+ |
//! was the only thing standing between a rebrand and a stale 404.
|
|
9 |
+ |
//!
|
|
10 |
+ |
//! Embedding them makes the pages ride the binary, so they cannot be a release
|
|
11 |
+ |
//! behind it. Caddy proxies its own 404/500 here (see `deploy/Caddyfile`).
|
|
12 |
+ |
//!
|
|
13 |
+ |
//! `502.html` stays on disk and is deliberately NOT reachable through this
|
|
14 |
+ |
//! route: 502 is the app-is-down page, and a page the app has to serve is
|
|
15 |
+ |
//! exactly the page that will not render when it is needed. Sando ships the
|
|
16 |
+ |
//! directory as a release sibling (`release_contents` in the daemon config), so
|
|
17 |
+ |
//! Caddy keeps a disk copy for that one case.
|
|
18 |
+ |
|
|
19 |
+ |
use axum::{
|
|
20 |
+ |
extract::Path,
|
|
21 |
+ |
http::{StatusCode, header},
|
|
22 |
+ |
response::{IntoResponse, Response},
|
|
23 |
+ |
};
|
|
24 |
+ |
|
|
25 |
+ |
/// Embedded relative to the crate root so the constant does not depend on the
|
|
26 |
+ |
/// process working directory (same pattern as `deploy_lint::CADDYFILE`).
|
|
27 |
+ |
const PAGE_404: &str = include_str!(concat!(
|
|
28 |
+ |
env!("CARGO_MANIFEST_DIR"),
|
|
29 |
+ |
"/deploy/error-pages/404.html"
|
|
30 |
+ |
));
|
|
31 |
+ |
const PAGE_500: &str = include_str!(concat!(
|
|
32 |
+ |
env!("CARGO_MANIFEST_DIR"),
|
|
33 |
+ |
"/deploy/error-pages/500.html"
|
|
34 |
+ |
));
|
|
35 |
+ |
|
|
36 |
+ |
/// Serve an embedded error page under its own status code.
|
|
37 |
+ |
///
|
|
38 |
+ |
/// The status is echoed rather than 200 because Caddy's `handle_errors` block
|
|
39 |
+ |
/// forwards the upstream status to the client; answering 200 would turn every
|
|
40 |
+ |
/// Caddy-generated 404 into a soft-404 for crawlers.
|
|
41 |
+ |
///
|
|
42 |
+ |
/// An unknown name is itself a 404 with the 404 page, which is the only
|
|
43 |
+ |
/// coherent answer: there is nothing else to say about `/__errors/tea.html`.
|
|
44 |
+ |
pub(super) async fn error_page(Path(name): Path<String>) -> Response {
|
|
45 |
+ |
let (status, body) = match name.as_str() {
|
|
46 |
+ |
"500.html" => (StatusCode::INTERNAL_SERVER_ERROR, PAGE_500),
|
|
47 |
+ |
_ => (StatusCode::NOT_FOUND, PAGE_404),
|
|
48 |
+ |
};
|
|
49 |
+ |
(
|
|
50 |
+ |
status,
|
|
51 |
+ |
[(header::CONTENT_TYPE, "text/html; charset=utf-8")],
|
|
52 |
+ |
body,
|
|
53 |
+ |
)
|
|
54 |
+ |
.into_response()
|
|
55 |
+ |
}
|
|
56 |
+ |
|
|
57 |
+ |
#[cfg(test)]
|
|
58 |
+ |
mod tests {
|
|
59 |
+ |
use super::*;
|
|
60 |
+ |
|
|
61 |
+ |
/// The embed is only useful if it actually caught the real pages. A path
|
|
62 |
+ |
/// typo would compile-fail, but an empty or truncated file would not.
|
|
63 |
+ |
#[test]
|
|
64 |
+ |
fn embedded_pages_are_the_real_pages() {
|
|
65 |
+ |
for page in [PAGE_404, PAGE_500] {
|
|
66 |
+ |
assert!(page.starts_with("<!DOCTYPE html>"), "not an HTML document");
|
|
67 |
+ |
assert!(page.contains("makenot.work"), "missing the wordmark");
|
|
68 |
+ |
}
|
|
69 |
+ |
assert!(PAGE_404.contains("404"));
|
|
70 |
+ |
assert!(PAGE_500.contains("500"));
|
|
71 |
+ |
}
|
|
72 |
+ |
|
|
73 |
+ |
/// 502 must not be reachable here — see the module docs. This is the test
|
|
74 |
+ |
/// that fails if someone "completes the set" later.
|
|
75 |
+ |
#[tokio::test]
|
|
76 |
+ |
async fn unknown_names_including_502_serve_the_404_page() {
|
|
77 |
+ |
for name in ["502.html", "tea.html", "../../etc/passwd"] {
|
|
78 |
+ |
let res = error_page(Path(name.to_string())).await;
|
|
79 |
+ |
assert_eq!(res.status(), StatusCode::NOT_FOUND, "{name}");
|
|
80 |
+ |
}
|
|
81 |
+ |
}
|
|
82 |
+ |
|
|
83 |
+ |
#[tokio::test]
|
|
84 |
+ |
async fn status_matches_the_page() {
|
|
85 |
+ |
let res = error_page(Path("500.html".to_string())).await;
|
|
86 |
+ |
assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
|
87 |
+ |
}
|
|
88 |
+ |
}
|