//! `/__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);
}
}