From 5af999cadb8cddd055eec395cff318462c2c6fb0 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Wed, 17 Dec 2025 12:40:53 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Mejora=20gesti=C3=B3n=20de=20err?= =?UTF-8?q?ores=20para=20403,=20404=20y=20otros?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/theme/definition.rs | 78 +++++++++++++++++++++---- src/locale/en-US/theme.ftl | 31 +++++++++- src/locale/es-ES/theme.ftl | 31 +++++++++- src/response/page/error.rs | 108 ++++++++++++++++++++--------------- 4 files changed, 185 insertions(+), 63 deletions(-) diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index a0edbd9a..81e47756 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -1,10 +1,12 @@ -use crate::core::component::Contextual; +use crate::base::component::{Html, Intro, IntroOpening}; +use crate::core::component::{Child, ChildOp, Component, Contextual}; use crate::core::extension::Extension; +use crate::core::theme::{DefaultRegion, DefaultTemplate, TemplateRef}; use crate::global; use crate::html::{html, Markup}; use crate::locale::L10n; -use crate::prelude::{DefaultTemplate, TemplateRef}; use crate::response::page::Page; +use crate::service::http::StatusCode; /// Interfaz común que debe implementar cualquier tema de PageTop. /// @@ -155,20 +157,76 @@ pub trait Theme: Extension + Send + Sync { } } - /// Contenido predeterminado para la página de error "*403 - Forbidden*". + /// Contenido predefinido para la página de error "*403 - Forbidden*" (acceso denegado). /// /// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la - /// página de error, manteniendo o no el mensaje de los *textos localizados*. - fn error403(&self, page: &mut Page) -> Markup { - html! { div { h1 { (L10n::l("error403_notice").using(page)) } } } + /// página de error. + fn error_403(&self, page: &mut Page) { + page.alter_title(L10n::l("error403_title")) + .alter_template(&DefaultTemplate::Error) + .alter_child_in( + &DefaultRegion::Content, + ChildOp::Prepend(Child::with(Html::with(move |cx| { + html! { + div { + h1 { (L10n::l("error403_alert").using(cx)) } + p { (L10n::l("error403_help").using(cx)) } + } + } + }))), + ); } - /// Contenido predeterminado para la página de error "*404 - Not Found*". + /// Contenido predefinido para la página de error "*404 - Not Found*" (recurso no encontrado). /// /// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la - /// página de error, manteniendo o no el mensaje de los *textos localizados*. - fn error404(&self, page: &mut Page) -> Markup { - html! { div { h1 { (L10n::l("error404_notice").using(page)) } } } + /// página de error. + fn error_404(&self, page: &mut Page) { + page.alter_title(L10n::l("error404_title")) + .alter_template(&DefaultTemplate::Error) + .alter_child_in( + &DefaultRegion::Content, + ChildOp::Prepend(Child::with(Html::with(move |cx| { + html! { + div { + h1 { (L10n::l("error404_alert").using(cx)) } + p { (L10n::l("error404_help").using(cx)) } + } + } + }))), + ); + } + + /// Permite al tema preparar y componer una página de error fatal. + /// + /// Por defecto, asigna el título al documento (`title`) y muestra un componente [`Intro`] con + /// el código HTTP del error (`code`) y los mensajes proporcionados (`alert` y `help`) como + /// descripción del error. + /// + /// Este método no se utiliza en las implementaciones predefinidas de [`Self::error_403()`] ni + /// [`Self::error_404()`], que definen su propio contenido específico. + /// + /// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la + /// página de error. + fn error_fatal(&self, page: &mut Page, code: StatusCode, title: L10n, alert: L10n, help: L10n) { + page.alter_title(title) + .alter_template(&DefaultTemplate::Error) + .alter_child_in( + &DefaultRegion::Content, + ChildOp::Prepend(Child::with( + Intro::new() + .with_title(L10n::l("error_code").with_arg("code", code.as_str())) + .with_slogan(L10n::n(code.to_string())) + .with_button(None) + .with_opening(IntroOpening::Custom) + .add_child(Html::with(move |cx| { + html! { + h1 { (alert.using(cx)) } + p { (help.using(cx)) } + } + })), + )), + ); } } diff --git a/src/locale/en-US/theme.ftl b/src/locale/en-US/theme.ftl index f766766d..3f4c0064 100644 --- a/src/locale/en-US/theme.ftl +++ b/src/locale/en-US/theme.ftl @@ -3,7 +3,32 @@ region_header = Header region_content = Content region_footer = Footer -error403_notice = FORBIDDEN ACCESS -error404_notice = RESOURCE NOT FOUND - +# Logo. pagetop_logo = PageTop Logo + +# Error Messages. +error_code = Error { $code } + +error400_title = Error BAD REQUEST +error400_alert = The request could not be processed. +error400_help = The server could not understand your request. The address may be incorrect or some required data may be missing. + +error403_title = Error FORBIDDEN +error403_alert = You do not have permission to access this resource. +error403_help = Your account does not have the necessary privileges to view this page. If you believe this is an error, please contact the system administrator. + +error404_title = Error RESOURCE NOT FOUND +error404_alert = The requested page could not be found. +error404_help = The address may be incorrect, or the document may have been moved or deleted. Check the URL or use the navigation links to return to a known place. + +error500_title = Error INTERNAL ERROR +error500_alert = An unexpected error occurred on the server. +error500_help = We could not complete your request due to an internal problem. Please try again in a few minutes. If the error persists, contact the system administrator. + +error503_title = Error SERVICE UNAVAILABLE +error503_alert = The service is temporarily unavailable. +error503_help = The server is currently unable to handle your request due to maintenance or high load. Please try again in a few minutes. If the problem persists, contact the system administrator. + +error504_title = Error GATEWAY TIMEOUT +error504_alert = The server took too long to respond. +error504_help = The service is temporarily unavailable or overloaded. Please try again in a few minutes. If the problem continues, notify the system administrator. diff --git a/src/locale/es-ES/theme.ftl b/src/locale/es-ES/theme.ftl index b8b91449..7d4abcf6 100644 --- a/src/locale/es-ES/theme.ftl +++ b/src/locale/es-ES/theme.ftl @@ -3,7 +3,32 @@ region_header = Cabecera region_content = Contenido region_footer = Pie de página -error403_notice = ACCESO NO PERMITIDO -error404_notice = RECURSO NO ENCONTRADO - +# Logo. pagetop_logo = Logotipo de PageTop + +# Error Messages. +error_code = Error { $code } + +error400_title = Error PETICIÓN INCORRECTA +error400_alert = No se ha podido procesar la petición. +error400_help = El servidor no ha podido interpretar su petición. Es posible que la dirección sea incorrecta o que falten datos obligatorios. Revise la información introducida e inténtelo de nuevo. + +error403_title = Error ACCESO PROHIBIDO +error403_alert = No dispone de permisos para acceder a este recurso. +error403_help = Su cuenta no tiene los privilegios necesarios para visualizar esta página. Si considera que se trata de un error, póngase en contacto con el administrador del sistema. + +error404_title = Error RECURSO NO ENCONTRADO +error404_alert = No se ha podido encontrar el recurso solicitado. +error404_help = Es posible que la dirección sea incorrecta o que el documento haya sido movido o eliminado. Compruebe la URL o utilice los enlaces de navegación para volver a una ubicación conocida. + +error500_title = Error INTERNO DEL SERVIDOR +error500_alert = Se ha producido un error interno en el servidor. +error500_help = No hemos podido completar su petición debido a un problema interno. Inténtelo de nuevo pasados unos minutos. Si el error persiste, póngase en contacto con el administrador del sistema. + +error503_title = Error SERVICIO NO DISPONIBLE +error503_alert = El servicio no está disponible temporalmente. +error503_help = En este momento el servidor no puede atender su petición debido a tareas de mantenimiento o a una alta carga de trabajo. Inténtelo de nuevo en unos minutos. Si el problema persiste, póngase en contacto con el administrador del sistema. + +error504_title = Error TIEMPO DE ESPERA AGOTADO +error504_alert = El servidor ha tardado demasiado en responder. +error504_help = El servicio no está disponible temporalmente o está experimentando una alta carga. Inténtelo de nuevo en unos minutos. Si el problema continúa, notifique la incidencia al administrador del sistema. diff --git a/src/response/page/error.rs b/src/response/page/error.rs index 7d6cf33b..fd9959c2 100644 --- a/src/response/page/error.rs +++ b/src/response/page/error.rs @@ -1,10 +1,9 @@ -use crate::base::component::Html; use crate::core::component::Contextual; -use crate::core::theme::DefaultTemplate; use crate::locale::L10n; use crate::response::ResponseError; use crate::service::http::{header::ContentType, StatusCode}; use crate::service::{HttpRequest, HttpResponse}; +use crate::util; use super::Page; @@ -12,71 +11,87 @@ use std::fmt; /// Página de error asociada a un código de estado HTTP. /// -/// Este enumerado agrupa los distintos tipos de error que pueden devolverse como página HTML -/// completa. Cada variante encapsula la solicitud original ([`HttpRequest`]) y se corresponde con -/// un código de estado concreto. +/// Este enumerado agrupa tipos esenciales de error que pueden devolverse como página HTML completa. +/// Cada variante encapsula la solicitud original ([`HttpRequest`]) y se corresponde con un código +/// de estado concreto. /// -/// Para algunos errores (como [`ErrorPage::AccessDenied`] y [`ErrorPage::NotFound`]) se construye -/// una [`Page`] usando la plantilla de error del tema activo ([`DefaultTemplate::Error`]), lo que -/// permite personalizar el contenido del mensaje. En el resto de casos se devuelve un cuerpo HTML -/// mínimo basado en una descripción genérica del error. -/// -/// `ErrorPage` implementa [`ResponseError`], por lo que puede utilizarse directamente como tipo de -/// error en los controladores HTTP. +/// Para cada error se construye una [`Page`] usando el tema activo, lo que permite personalizar +/// la plantilla y el contenido del mensaje mediante los métodos específicos del tema +/// (por ejemplo, [`Theme::error_403()`](crate::core::theme::Theme::error_403), +/// [`Theme::error_404()`](crate::core::theme::Theme::error_404) o +/// [`Theme::error_fatal()`](crate::core::theme::Theme::error_fatal)). #[derive(Debug)] pub enum ErrorPage { - NotModified(HttpRequest), BadRequest(HttpRequest), AccessDenied(HttpRequest), NotFound(HttpRequest), - PreconditionFailed(HttpRequest), InternalError(HttpRequest), - Timeout(HttpRequest), + ServiceUnavailable(HttpRequest), + GatewayTimeout(HttpRequest), +} + +impl ErrorPage { + /// Función auxiliar para renderizar una página de error genérica usando el tema activo. + /// + /// Construye una [`Page`] a partir de la petición y un prefijo de clave basado en el código de + /// estado (`error`), del que se derivan los textos localizados `error_title`, + /// `error_alert` y `error_help`. + /// + /// Si el renderizado falla, escribe en su lugar el texto plano asociado al código de estado. + fn display_error_page(&self, f: &mut fmt::Formatter<'_>, request: &HttpRequest) -> fmt::Result { + let mut page = Page::new(request.clone()); + let code = self.status_code(); + page.theme().error_fatal( + &mut page, + code, + L10n::l(util::join!("error", code.as_str(), "_title")), + L10n::l(util::join!("error", code.as_str(), "_alert")), + L10n::l(util::join!("error", code.as_str(), "_help")), + ); + if let Ok(rendered) = page.render() { + write!(f, "{}", rendered.into_string()) + } else { + f.write_str(&code.to_string()) + } + } } impl fmt::Display for ErrorPage { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - // Error 304. - ErrorPage::NotModified(_) => f.write_str("Not Modified"), // Error 400. - ErrorPage::BadRequest(_) => f.write_str("Bad Client Data"), + Self::BadRequest(request) => self.display_error_page(f, request), + // Error 403. - ErrorPage::AccessDenied(request) => { - let mut error_page = Page::new(request.clone()); - let error403 = error_page.theme().error403(&mut error_page); - if let Ok(page) = error_page - .with_title(L10n::n("Error FORBIDDEN")) - .with_template(&DefaultTemplate::Error) - .add_child(Html::with(move |_| error403.clone())) - .render() - { - write!(f, "{}", page.into_string()) + Self::AccessDenied(request) => { + let mut page = Page::new(request.clone()); + page.theme().error_403(&mut page); + if let Ok(rendered) = page.render() { + write!(f, "{}", rendered.into_string()) } else { - f.write_str("Access Denied") + f.write_str(&self.status_code().to_string()) } } + // Error 404. - ErrorPage::NotFound(request) => { - let mut error_page = Page::new(request.clone()); - let error404 = error_page.theme().error404(&mut error_page); - if let Ok(page) = error_page - .with_title(L10n::n("Error RESOURCE NOT FOUND")) - .with_template(&DefaultTemplate::Error) - .add_child(Html::with(move |_| error404.clone())) - .render() - { - write!(f, "{}", page.into_string()) + Self::NotFound(request) => { + let mut page = Page::new(request.clone()); + page.theme().error_404(&mut page); + if let Ok(rendered) = page.render() { + write!(f, "{}", rendered.into_string()) } else { - f.write_str("Not Found") + f.write_str(&self.status_code().to_string()) } } - // Error 412. - ErrorPage::PreconditionFailed(_) => f.write_str("Precondition Failed"), + // Error 500. - ErrorPage::InternalError(_) => f.write_str("Internal Error"), + Self::InternalError(request) => self.display_error_page(f, request), + + // Error 503. + Self::ServiceUnavailable(request) => self.display_error_page(f, request), + // Error 504. - ErrorPage::Timeout(_) => f.write_str("Timeout"), + Self::GatewayTimeout(request) => self.display_error_page(f, request), } } } @@ -91,13 +106,12 @@ impl ResponseError for ErrorPage { #[rustfmt::skip] fn status_code(&self) -> StatusCode { match self { - ErrorPage::NotModified(_) => StatusCode::NOT_MODIFIED, ErrorPage::BadRequest(_) => StatusCode::BAD_REQUEST, ErrorPage::AccessDenied(_) => StatusCode::FORBIDDEN, ErrorPage::NotFound(_) => StatusCode::NOT_FOUND, - ErrorPage::PreconditionFailed(_) => StatusCode::PRECONDITION_FAILED, ErrorPage::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, - ErrorPage::Timeout(_) => StatusCode::GATEWAY_TIMEOUT, + ErrorPage::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE, + ErrorPage::GatewayTimeout(_) => StatusCode::GATEWAY_TIMEOUT, } } }