use std::time::Duration;
use crate::axum::body::Body;
use crate::axum::http::{Response, StatusCode, header};
const RETRY_AFTER: u64 = 1;
const ERROR_RETRY_AFTER: u64 = 2;
#[must_use]
pub fn building(waited: Duration) -> Response<Body> {
page(
StatusCode::SERVICE_UNAVAILABLE,
RETRY_AFTER,
"Building",
&format!(
"<p>The application is rebuilding. This request waited \
{:.1}s and will be answered as soon as the new binary is up.</p>",
waited.as_secs_f32()
),
)
}
#[must_use]
pub fn compile_error(diagnostics: &str) -> Response<Body> {
page(
StatusCode::INTERNAL_SERVER_ERROR,
ERROR_RETRY_AFTER,
"Compile error",
&format!(
"<p>The application did not compile. This page reloads itself \
when it does.</p><pre>{}</pre>",
escape(diagnostics)
),
)
}
#[must_use]
pub fn backend_gone(reason: &str) -> Response<Body> {
page(
StatusCode::BAD_GATEWAY,
ERROR_RETRY_AFTER,
"Backend unavailable",
&format!(
"<p>The application process stopped answering.</p><pre>{}</pre>",
escape(reason)
),
)
}
fn page(status: StatusCode, retry_after: u64, title: &str, body: &str) -> Response<Body> {
let html = format!(
"<!doctype html>\n\
<html lang=\"en\"><head>\
<meta charset=\"utf-8\">\
<meta http-equiv=\"refresh\" content=\"{retry_after}\">\
<title>{title} -- arc dev</title>\
<style>body{{font:14px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;\
margin:3rem auto;max-width:60rem;padding:0 1.5rem}}\
h1{{font-size:1rem;font-weight:600}}\
pre{{white-space:pre-wrap;overflow-x:auto;padding:1rem;\
border:1px solid currentColor;border-radius:4px}}</style>\
</head><body><h1>{title}</h1>{body}\
<p><small>arc dev</small></p></body></html>\n"
);
Response::builder()
.status(status)
.header(header::CONTENT_TYPE, "text/html; charset=utf-8")
.header(header::RETRY_AFTER, retry_after.to_string())
.header(header::CACHE_CONTROL, "no-store")
.header("x-content-type-options", "nosniff")
.body(Body::from(html))
.unwrap_or_else(|_| {
let mut fallback = Response::new(Body::from("arc dev: the application is unavailable"));
*fallback.status_mut() = status;
fallback
})
}
fn escape(raw: &str) -> String {
let mut out = String::with_capacity(raw.len() + raw.len() / 8);
for ch in raw.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
other => out.push(other),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generic_parameters_in_a_diagnostic_do_not_eat_the_rest_of_the_line() {
let escaped = escape("expected `Vec<String>`, found `&str`");
assert!(!escaped.contains('<'), "{escaped}");
assert!(escaped.contains("<String>"), "{escaped}");
assert!(escaped.contains("&str"), "{escaped}");
}
#[test]
fn the_holding_page_asks_the_browser_to_come_back() {
let response = building(Duration::from_secs(5));
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(
response
.headers()
.get(header::RETRY_AFTER)
.and_then(|value| value.to_str().ok()),
Some("1")
);
}
#[test]
fn the_holding_page_is_never_cached() {
for response in [
building(Duration::from_secs(1)),
compile_error("error[E0425]"),
backend_gone("closed"),
] {
assert_eq!(
response
.headers()
.get(header::CACHE_CONTROL)
.and_then(|value| value.to_str().ok()),
Some("no-store")
);
}
}
#[test]
fn a_compile_error_page_reports_a_failure_not_a_wait() {
let response = compile_error("error[E0308]: mismatched types");
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[test]
fn a_dead_backend_is_a_gateway_failure_not_a_build() {
let response = backend_gone("ipc send failed");
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
}
#[test]
fn every_page_recovers_without_a_manual_reload() {
for response in [
building(Duration::from_secs(1)),
compile_error("error"),
backend_gone("closed"),
] {
assert_eq!(
response
.headers()
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok()),
Some("text/html; charset=utf-8")
);
}
}
}