//! `/__errors/{status}.html`: the branded error pages, embedded in the binary. //! //! These three pages used to live only as files that `deploy.sh` scp'd to //! `/opt/makenotwork/error-pages/` on every deploy, and Caddy's `handle_errors` //! read them from disk. That coupled a per-deploy file upload to a binary that //! versions with the copy and the brand glyphs the pages carry: the pages could //! drift a release behind the site they front, and a forgotten `--config` run //! was the only thing standing between a rebrand and a stale 404. //! //! Embedding them makes the pages ride the binary, so they cannot be a release //! behind it. Caddy proxies its own 404/500 here (see `deploy/Caddyfile`). //! //! `502.html` stays on disk and is deliberately NOT reachable through this //! route: 502 is the app-is-down page, and a page the app has to serve is //! exactly the page that will not render when it is needed. Sando ships the //! directory as a release sibling (`release_contents` in the daemon config), so //! Caddy keeps a disk copy for that one case. use axum::{ extract::Path, http::{StatusCode, header}, response::{IntoResponse, Response}, }; /// Embedded relative to the crate root so the constant does not depend on the /// process working directory (same pattern as `deploy_lint::CADDYFILE`). const PAGE_404: &str = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/deploy/error-pages/404.html" )); const PAGE_500: &str = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/deploy/error-pages/500.html" )); /// Serve an embedded error page under its own status code. /// /// The status is echoed rather than 200 because Caddy's `handle_errors` block /// forwards the upstream status to the client; answering 200 would turn every /// Caddy-generated 404 into a soft-404 for crawlers. /// /// An unknown name is itself a 404 with the 404 page, which is the only /// coherent answer: there is nothing else to say about `/__errors/tea.html`. pub(super) async fn error_page(Path(name): Path) -> Response { let (status, body) = match name.as_str() { "500.html" => (StatusCode::INTERNAL_SERVER_ERROR, PAGE_500), _ => (StatusCode::NOT_FOUND, PAGE_404), }; ( status, [(header::CONTENT_TYPE, "text/html; charset=utf-8")], body, ) .into_response() } #[cfg(test)] mod tests { use super::*; /// The embed is only useful if it actually caught the real pages. A path /// typo would compile-fail, but an empty or truncated file would not. #[test] fn embedded_pages_are_the_real_pages() { for page in [PAGE_404, PAGE_500] { assert!(page.starts_with(""), "not an HTML document"); assert!(page.contains("makenot.work"), "missing the wordmark"); } assert!(PAGE_404.contains("404")); assert!(PAGE_500.contains("500")); } /// 502 must not be reachable here. See the module docs. This is the test /// that fails if someone "completes the set" later. #[tokio::test] async fn unknown_names_including_502_serve_the_404_page() { for name in ["502.html", "tea.html", "../../etc/passwd"] { let res = error_page(Path(name.to_string())).await; assert_eq!(res.status(), StatusCode::NOT_FOUND, "{name}"); } } #[tokio::test] async fn status_matches_the_page() { let res = error_page(Path("500.html".to_string())).await; assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); } }