use axum::http::StatusCode;
pub const MIN_ERROR_CODE: u16 = 400;
pub const MAX_ERROR_CODE: u16 = 599;
const UNKNOWN_REASON: &str = "Unknown Error";
const UNKNOWN_DESCRIPTION: &str = "An unexpected error occurred while processing the request.";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
Html,
Json,
Xml,
Text,
}
pub fn is_usable_code(code: u16) -> bool {
(MIN_ERROR_CODE..=MAX_ERROR_CODE).contains(&code)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodeSource {
Path,
Header,
Default,
}
pub fn resolve_code_with_source(
path: &str,
x_code: Option<&str>,
default: u16,
) -> (u16, CodeSource) {
debug_assert!(
is_usable_code(default),
"el código por defecto debe estar en 400..=599: {default}"
);
if let Some(code) = code_from_path(path).filter(|code| is_usable_code(*code)) {
return (code, CodeSource::Path);
}
if let Some(code) = x_code
.and_then(parse_code)
.filter(|code| is_usable_code(*code))
{
return (code, CodeSource::Header);
}
(default, CodeSource::Default)
}
pub fn code_from_path(path: &str) -> Option<u16> {
let first_segment = path
.trim_start_matches('/')
.split('/')
.next()
.unwrap_or_default();
let stem = match first_segment.rsplit_once('.') {
Some((stem, extension)) if format_of_extension(extension).is_some() => stem,
_ => first_segment,
};
parse_code(stem)
}
pub fn resolve_format(
path: &str,
content_type: Option<&str>,
x_format: Option<&str>,
accept: Option<&str>,
) -> Format {
if let Some(format) = format_of_path(path) {
return format;
}
if let Some(format) = content_type.and_then(format_of_mime) {
return format;
}
if let Some(format) = x_format.and_then(format_of_mime) {
return format;
}
if let Some(format) = accept.and_then(format_of_accept) {
return format;
}
Format::Text
}
pub fn canonical_reason(code: u16) -> &'static str {
StatusCode::from_u16(code)
.ok()
.and_then(|status| status.canonical_reason())
.unwrap_or(UNKNOWN_REASON)
}
pub fn description(code: u16) -> &'static str {
match code {
400 => "The server could not understand the request because of invalid syntax.",
401 => "Authentication is required and has either failed or not been provided.",
403 => "The server understood the request but refuses to authorise it.",
404 => "The requested resource could not be found on this server.",
405 => "The request method is known by the server but is not supported by the resource.",
408 => "The server timed out waiting for the request.",
410 => "The resource requested is no longer available and will not be available again.",
418 => "The server is a teapot: it refuses to brew coffee.",
422 => "The request was well-formed but could not be processed due to semantic errors.",
429 => {
"Too many requests have been sent in a given amount of time. Please try again later."
}
500 => "The server encountered an unexpected condition and could not complete the request.",
501 => "The server does not support the functionality required to fulfil the request.",
502 => "The server received an invalid response from an upstream server.",
503 => {
"The service is temporarily unavailable, usually because of maintenance or overload. \
Please try again later."
}
504 => "The server did not receive a timely response from an upstream server.",
_ => UNKNOWN_DESCRIPTION,
}
}
pub fn retry_after_secs(code: u16, configured: u32) -> Option<u32> {
match code {
429 | 503 => Some(configured),
_ => None,
}
}
fn parse_code(raw: &str) -> Option<u16> {
raw.parse::<u16>().ok()
}
fn format_of_path(path: &str) -> Option<Format> {
let last_segment = path
.trim_start_matches('/')
.rsplit('/')
.next()
.unwrap_or_default();
let (_, extension) = last_segment.rsplit_once('.')?;
format_of_extension(extension)
}
fn format_of_extension(extension: &str) -> Option<Format> {
match extension.to_ascii_lowercase().as_str() {
"html" | "htm" => Some(Format::Html),
"json" => Some(Format::Json),
"xml" => Some(Format::Xml),
"txt" => Some(Format::Text),
_ => None,
}
}
fn format_of_mime(value: &str) -> Option<Format> {
let mime = match value.split(';').next() {
Some(mime) => mime.trim(),
None => "",
};
match mime.to_ascii_lowercase().as_str() {
"text/html" | "application/xhtml+xml" => Some(Format::Html),
"application/json" => Some(Format::Json),
"application/xml" | "text/xml" => Some(Format::Xml),
"text/plain" => Some(Format::Text),
_ => None,
}
}
fn format_of_accept(value: &str) -> Option<Format> {
value.split(',').find_map(format_of_mime)
}
#[cfg(test)]
mod tests {
use super::*;
fn resolve_code(path: &str, x_code: Option<&str>, default: u16) -> u16 {
resolve_code_with_source(path, x_code, default).0
}
#[test]
fn el_codigo_del_path_se_resuelve() {
assert_eq!(resolve_code("/404", None, 404), 404);
assert_eq!(resolve_code("/503", None, 404), 503);
}
#[test]
fn el_path_gana_a_la_cabecera_x_code() {
assert_eq!(resolve_code("/404", Some("500"), 404), 404);
}
#[test]
fn sin_codigo_en_el_path_gana_la_cabecera() {
assert_eq!(resolve_code("/", Some("503"), 404), 503);
}
#[test]
fn sin_pistas_gana_el_defecto() {
assert_eq!(resolve_code("/", None, 404), 404);
assert_eq!(resolve_code("/", None, 418), 418);
}
#[test]
fn un_path_no_numerico_no_resuelve_por_esa_via() {
assert_eq!(resolve_code("/foo", Some("500"), 404), 500);
assert_eq!(resolve_code("/foo", None, 404), 404);
assert_eq!(resolve_code("/", Some("no-es-un-numero"), 404), 404);
assert_eq!(resolve_code("", None, 404), 404);
}
#[test]
fn la_extension_no_confunde_al_codigo() {
assert_eq!(resolve_code("/404.html", None, 404), 404);
assert_eq!(resolve_code("/404.json", None, 404), 404);
assert_eq!(resolve_code("/503.xml", None, 404), 503);
assert_eq!(resolve_code("/404.txt", None, 404), 404);
assert_eq!(resolve_code("/404.htm", None, 404), 404);
}
#[test]
fn el_rango_utilizable_es_400_599() {
assert!(!is_usable_code(0));
assert!(!is_usable_code(100));
assert!(!is_usable_code(200));
assert!(!is_usable_code(204));
assert!(!is_usable_code(304));
assert!(!is_usable_code(399));
assert!(is_usable_code(400));
assert!(is_usable_code(503));
assert!(is_usable_code(599));
assert!(!is_usable_code(600));
assert!(!is_usable_code(1000));
}
#[test]
fn un_codigo_no_utilizable_en_el_path_cae_al_defecto() {
assert_eq!(resolve_code("/200", None, 404), 404);
assert_eq!(resolve_code("/100", None, 404), 404);
assert_eq!(resolve_code("/204", None, 404), 404);
assert_eq!(resolve_code("/304", None, 404), 404);
assert_eq!(resolve_code("/1000", None, 404), 404);
assert_eq!(resolve_code("/200.html", None, 404), 404);
}
#[test]
fn un_codigo_no_utilizable_en_el_path_deja_paso_a_la_cabecera() {
assert_eq!(resolve_code("/200", Some("503"), 404), 503);
assert_eq!(resolve_code("/100", Some("418"), 404), 418);
}
#[test]
fn un_x_code_no_utilizable_cae_al_defecto() {
assert_eq!(resolve_code("/", Some("200"), 404), 404);
assert_eq!(resolve_code("/", Some("0"), 404), 404);
assert_eq!(resolve_code("/", Some("100"), 404), 404);
assert_eq!(resolve_code("/", Some("1000"), 404), 404);
assert_eq!(resolve_code("/", Some("abc"), 404), 404);
assert_eq!(resolve_code("/", Some(""), 404), 404);
}
#[test]
fn un_path_con_forma_de_codigo_lo_aporta_aunque_no_sea_utilizable() {
assert_eq!(code_from_path("/200"), Some(200));
assert_eq!(code_from_path("/200.html"), Some(200));
assert_eq!(code_from_path("/404.html"), Some(404));
}
#[test]
fn la_extension_manda() {
assert_eq!(resolve_format("/404.json", None, None, None), Format::Json);
assert_eq!(resolve_format("/404.xml", None, None, None), Format::Xml);
assert_eq!(resolve_format("/404.txt", None, None, None), Format::Text);
assert_eq!(resolve_format("/404.html", None, None, None), Format::Html);
assert_eq!(resolve_format("/404.htm", None, None, None), Format::Html);
}
#[test]
fn el_content_type_de_la_peticion_decide() {
assert_eq!(
resolve_format("/404", Some("application/json"), None, None),
Format::Json
);
}
#[test]
fn x_format_decide_sin_extension() {
assert_eq!(
resolve_format("/404", None, Some("application/xml"), None),
Format::Xml
);
}
#[test]
fn accept_decide_faltando_lo_demas() {
assert_eq!(
resolve_format("/404", None, None, Some("text/html")),
Format::Html
);
assert_eq!(
resolve_format("/404", None, None, Some("application/json")),
Format::Json
);
}
#[test]
fn un_accept_de_navegador_real_cae_en_html() {
let accept =
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8";
assert_eq!(
resolve_format("/404", None, None, Some(accept)),
Format::Html
);
}
#[test]
fn un_content_type_con_charset_decide() {
assert_eq!(
resolve_format("/404", Some("application/json; charset=utf-8"), None, None),
Format::Json
);
}
#[test]
fn sin_pistas_el_defecto_es_texto_plano() {
assert_eq!(resolve_format("/404", None, None, None), Format::Text);
assert_eq!(resolve_format("/", None, None, None), Format::Text);
}
#[test]
fn el_orden_de_la_cascada_se_respeta() {
assert_eq!(
resolve_format(
"/404.json",
Some("text/html"),
Some("application/xml"),
Some("text/html"),
),
Format::Json
);
assert_eq!(
resolve_format(
"/404",
Some("application/json"),
Some("application/xml"),
Some("text/html"),
),
Format::Json
);
assert_eq!(
resolve_format("/404", None, Some("application/xml"), Some("text/html")),
Format::Xml
);
}
#[test]
fn una_pista_no_soportada_cae_al_siguiente_origen() {
assert_eq!(
resolve_format("/404", Some("image/png"), None, Some("text/html")),
Format::Html
);
assert_eq!(
resolve_format("/404", None, Some("image/png"), Some("text/html")),
Format::Html
);
assert_eq!(
resolve_format("/404", None, None, Some("image/png")),
Format::Text
);
}
#[test]
fn codigo_y_formato_son_ortogonales_en_503_xml() {
assert_eq!(resolve_code("/503.xml", None, 404), 503);
assert_eq!(resolve_format("/503.xml", None, None, None), Format::Xml);
}
#[test]
fn la_razon_canonica_de_los_codigos_comunes() {
assert_eq!(canonical_reason(400), "Bad Request");
assert_eq!(canonical_reason(404), "Not Found");
assert_eq!(canonical_reason(418), "I'm a teapot");
assert_eq!(canonical_reason(500), "Internal Server Error");
assert_eq!(canonical_reason(503), "Service Unavailable");
assert!(!canonical_reason(599).is_empty());
}
#[test]
fn la_descripcion_nunca_esta_vacia() {
assert!(!description(404).is_empty());
assert!(!description(503).is_empty());
assert!(!description(599).is_empty());
}
#[test]
fn retry_after_solo_para_429_y_503() {
assert_eq!(retry_after_secs(429, 120), Some(120));
assert_eq!(retry_after_secs(503, 120), Some(120));
assert_eq!(retry_after_secs(404, 120), None);
assert_eq!(retry_after_secs(500, 120), None);
assert_eq!(retry_after_secs(429, 30), Some(30));
assert_eq!(retry_after_secs(503, 45), Some(45));
assert_eq!(retry_after_secs(404, 30), None);
assert_eq!(retry_after_secs(503, u32::MAX), Some(u32::MAX));
}
}