Skip to main content

max / makenotwork

3.5 KB · 89 lines History Blame Raw
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 }
89