Rusty Full Stack
El blog para los amantes de Rust, Ionic y Vuejs
El blog para los amantes de Rust, Ionic y Vuejs
Hola 😃
En nuestro artículo anterior de la serie sobre desarrollo web con Rust, continuamos haciendo el desarrollo de un mini blog utilizando Rust, actix-web y tera. En nuestro ejemplo, agregamos la página para ver los detalles de un post y para hacer la búsqueda del post utilizamos un id que pasamos como parámetro de URL. Por si no has leído el artículo, lo puedes encontrar acá:
Organizar Templates HTML con actix-web y tera.
En el ejemplo comentamos que existe un "caso de error" que debíamos manejar cuando un usuario busca un post que no existe, en ese caso, mostramos al usuario un mensaje bastante "feo" (de hecho ni siquiera es u HTML!) como este:
También nuestro código para el manejo de errores no era fácil de mantener a largo plazo, pero en este artículo vamos a mejorarlo y comenzar a aprender los fundamentos para el manejo de errores con actix-web.
Antes de empezar, quisiera hacer un pequeño disclaimer, en esta ocasión de mostraré el método básico mostrado en la documentación, con mi propia explicación claro 😉, pero existe aún una forma todavía más organizada y fácil de mantener sobre todo cuando se necesitan controlar muchos casos de error que puedan provocar crates externos, esa forma es utilizando el crate llamado thiserror, pero eso lo dejaremos para otro artículo, ya que lo mejor es comprender el "por qué" para luego poder mejorarlo.
Actix, nos provee con un tipo "Error" ya construido y que lo podemos encontrar en:
actix_web::error::Error
De igual manera nos provee un trait (por si vienes del mundo de programación a objetos, esto puedes verlo como una interfaz) el cual podemos implementar.
actix_web::error::ResponseError
Este trait nos permite implementar estos dos métodos:
pub trait ResponseError {
fn error_response(&self) -> HttpResponse<BoxBody>;
fn status_code(&self) -> StatusCode;
}
en donde:
fn error_response: nos permite poder decidir que hacer ante un error, por ejemplo redireccionar a una página personalizada cuando no se encuentra un post.
fn status_code: nos permite poder transformar o "mapear" un error que nosotros personalicemos a un status code del protocolo HTTP. Este error personalizado puedes compararlo con un "Exception" en otros lenguajes de programación, aunque en Rust, no se maneja ese concepto como tal.
A lo mejor puedas pensar que eso de los traits o interfaces suena complicado, pero por el contrario, nos permite manejar errores de forma super fácil porque prácticamente podemos crear un error personalizado casi que con lo que queramos como por ejemplo una struct o un enum! 😃
Por ejemplo podríamos construir algo así:
struct MiError {
nomobre: &'static str
}
impl actix_web::error::ResponseError for MiError {
fn error_response(&self) -> HttpResponse {
// Hacer algo con el error como una redireccion
}
fn status_code(&self) -> StatusCode {
// Mapear mi error como por ejemplo:
match *self {
MiError::NoEncontrato => StatusCode::NOT_FOUND
}
}
}
El ejemplo anterior aunque no correrá es para poder ejemplificar que podemos mantener nuestros errores organizados en el tipo de dato que nosotros queramos construir, es decir, que podemos utilizar tanto el método de la documentación como cualquier otro método de gestión de errores que prefiramos, siempre y cuando implementemos esos dos métodos todo estará bajo control 😎
Como siempre, todo es más claro con un ejemplo, pero no vamos a empezar desde cero, sino, que vamos a iniciar desde nos quedamos anteriormente, por si no tienes el código, puedes descargarlo desde este repositorio.
También puedes ver el código final acá.
Lo primero que vamos a hacer es agregar un crate bastante popular llamado derive_more. Lo puedes hacer directamente con "cargo add" o en el archivo Cargo.toml podemos colocar lo siguiente:
// Cargo.toml
[package]
name = "mi_blog"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-files = "0.6.5"
actix-web = "4.5.1"
derive_more = "0.99.17"
serde = {version="1.0.196", features=["derive"]}
tera = "1.19.1"
Prácticamente este será el único crate que agregaremos, ahora vamos a crear un archivo en el cual manejemos nuestros errores. Para ello vamos a crear un nuevo archivo dentro de "src" al que llamaremos manejo_errores.rs
Tu "src", debe verse de esta forma:
Ahora podemos crear dentro de nuestro nuevo archivo cualquier estructura o enum que nos permita implementar los métodos que queramos. El implementar estos métodos nos permitirán simplemente devolver un error desde nuestro controlador en lugar de explícitamente devolver un template HTML, esto es mucho más descriptivo y más fácil de poder rastrear en caso de errores.
En nuestro caso vamos a crear un enum, esto debido a que podremos relacionar cualquier valor del enum con un StatusCode. También vamos a agregar todos los imports que necesitemos, en el archivo src/manejo_errores.rs coloca el siguiente código:
// src/manejo_errores.rs
use actix_web::{error, Result};
use derive_more::{Display, Error};
#[derive(Debug, Display, Error)]
pub enum MiError {}
A este punto únicamente hemos realizado los imports que vamos a necesitar y creado un enum vacío. La línea:
#[derive(Debug, Display, Error)]
establece que el enum implementa un tipo "Error" y también un "Display", el cual también nos permitirá poder pasar algún mensaje que queramos mostrar en el error.
Ahora vamos a agregar nuestros errores personalizados en el enum MiError, en este caso podemos colocar tantos errores como nosotros queramos manejar, por ejemplo si no se encuentra un archivo estático, alguna página, alguna validación de usuario, etc.
Para nuestro ejemplo, por el momento, únicamente vamos a crear dos tipos, el primero será para manejar cuando no se encuentre el id de un post y el otro para un error general, a medida vayamos avanzando en esta serie, crearemos más tipos de error. Adicional a definir el error, vamos a pasarle un mensaje de error que vamos a mostrar al usuario. En el archivo src/manejo_errores.rs podemos colocar el siguiente código:
// src/manejo_errores.rs
use actix_web::{error, Result};
use derive_more::{Display, Error};
#[derive(Debug, Display, Error)]
pub enum MiError {
#[display(fmt = "No encontrado")]
PaginaNoEncontrada,
#[display(fmt = "Ha ocurrido un error, estamos investigando la causa")]
ErrorGeneral,
}
Hasta acá, simplemente hemos terminado de definir nuestro enum con dos posibles valores:
Lo más llamativo que podemos ver es el derive macro que hemos colocado sobre cada tipo, por ejemplo:
#[display(fmt = "No encontrado")]
La línea anterior es nada más que el mensaje de error que se le mostrará al usuario (fmt), todo lo que hemos hecho hasta el momento ha sido bastante normal, es solo crear el enum como cualquier otro enum con Rust. Ahora vamos a hacer "la magia" o más bien vamos a implementar los métodos de errores proporcionados por el trait de actix.
En el archivo src/manejo_errores.rs coloca el siguiente código:
// src/manejo_erorres.rs
use actix_web::{
error,
http::{header::ContentType, StatusCode},
HttpResponse
};
use derive_more::{Display, Error};
#[derive(Debug, Display, Error)]
pub enum MiError {
#[display(fmt = "No encontrado")]
PaginaNoEncontrada,
#[display(fmt = "Ha ocurrido un error, estamos investigando la causa")]
ErrorGeneral,
}
impl error::ResponseError for MiError {
fn error_response(&self) -> HttpResponse {
// A implementar en un momento
todo!()
}
fn status_code(&self) -> StatusCode {
// A implementar en un momento
todo!()
}
}
Es importante notar que hemos agregado algunos nuevos imports y vamos a implementar los métodos que hemos mencionado anteriormente.
Aunque deberíamos de iniciar con error_response, me voy a permitir empezar por el método status_code porque, pienso, es más sencillo de explicar. Ahora el código en el archivo src/manejo_errores.rs debe verse de esta forma:
// src/manejo_errores.rs
use actix_web::{
error,
http::{header::ContentType, StatusCode},
HttpResponse
};
use derive_more::{Display, Error};
#[derive(Debug, Display, Error)]
pub enum MiError {
#[display(fmt = "No encontrado")]
PaginaNoEncontrada,
#[display(fmt = "Ha ocurrido un error, estamos investigando la causa")]
ErrorGeneral,
}
impl error::ResponseError for MiError {
fn error_response(&self) -> HttpResponse {
// A implementar en un momento
todo!()
}
fn status_code(&self) -> StatusCode {
match *self {
MiError::PaginaNoEncontrada => StatusCode::NOT_FOUND,
MiError::ErrorGeneral => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
Del código anterior, la implementación del método status_code:
fn status_code(&self) -> StatusCode {
match *self {
MiError::PaginaNoEncontrada => StatusCode::NOT_FOUND,
MiError::ErrorGeneral => StatusCode::INTERNAL_SERVER_ERROR,
}
}
Se puede interpretar de la siguiente forma:
Si en el código, se emite un error (Err) de tipo MiError::PaginaNoEncontrada, entonces, a ese tipo de error colocarle un código HTTP 404 (NOT_FOUND)
Si en el código, se emite un error (Err) de tipo MiErro::ErrorGeneral, entonces, a ese tipo de error colocarle un código HTTP 500 (INTERNAL_SERVER_ERROR)
Ahora, pasemos a implementar el otro método, error_response, Algo importante a notar antes de implementarlo, es que el tipo de retorno es un HttpResponse, por lo que al detectarse cualquiera de los errores definidos, entonces actix-web, automáticamente, devolverá ese tipo de respuesta.
Lo que haremos ahora para implementar el método, es devolver un contenido HTML con el mensaje que hemos definido en nuestro enum para el tipo de error que se emita (fmt)
Para ello modifica el código de tu archivo src/manejo_errores.rs, para que se vea de esta forma:
// src/manejo_errores.rs
use actix_web::{
error,
http::{header::ContentType, StatusCode},
HttpResponse,
};
use derive_more::{Display, Error};
#[derive(Debug, Display, Error)]
pub enum MiError {
#[display(fmt = "No encontrado")]
PaginaNoEncontrada,
#[display(fmt = "Ha ocurrido un error, estamos investigando la causa")]
ErrorGeneral,
}
impl error::ResponseError for MiError {
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code())
.insert_header(ContentType::html())
.body(self.to_string())
}
fn status_code(&self) -> StatusCode {
match *self {
MiError::PaginaNoEncontrada => StatusCode::NOT_FOUND,
MiError::ErrorGeneral => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
Revisemos detenidamente la implementación del método error_response
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code())
.insert_header(ContentType::html())
.body(self.to_string())
}
Lo primero a revisar es que se va a devolver un HttpResponse y se ha construido de esta forma:
HttpResponse::build(self.status_code())
Esto nos permite ver, que dentro del error_response, podemos acceder al status code de la implementación del método status_code, lo que indica que incluso podríamos tomar decisiones basados en ello 🥳!
La siguiente línea:
.insert_header(ContentType::html())
Nos permite configurar el header de respuesta que en este caso estamos definiendo que retornaremos un contenido de tipo HTML.
Por último la línea:
.body(self.to_string())
Indica que el "body" de la respuesta, será el mensaje (self.to_string()) que hemos definido en nuestro enum MiError.
Ya estamos listos para poder manejar nuestro error de una mejor manera, vamos a ver un ejemplo. En nuestro archivo src/main.rs vamos a modificar nuestro código para agregar nuestro nuevo módulo de manejo de errores y vamos a utilizar nuestro enum para indicar que hay un error.
Por el momento, si un post no es encontrado, estás líneas de código manejan el error:
Pero, ahora lo haremos de una mejor maneja devolviendo un error. En el archivo src/main.rs, coloquemos el siguiente código:
// src/main.rs
mod archivos_estaticos;
mod manejo_errores;
mod posts;
use actix_web::{get, web, App, HttpResponse, HttpServer, Result};
use archivos_estaticos::leer_archivo_estatico;
use manejo_errores::MiError;
use posts::{obtener_post, obtener_todos_los_posts, Post};
use tera::{Context, Tera};
// Es importante notar que para acceder al State que se guardo
// con .app_data, se coloca como parametro en la definicion del metodo
// el web::Data<T> donde T es el tipo de dato que pasamos en .app_data
// Lo genial es que si no necesitamos utilizar Tera, simplemente podemos
// quitarlo de la definicion, es decir, no es obligatorio poner en las
// definicionnes de las funciones todo lo que se ecuentre en el state
#[get("/")]
async fn index(tera: web::Data<Tera>) -> HttpResponse {
// Ahora creamos el Context para pasarle valores a nuestro template HTML
// es importante notar que le agregamos un "mut" porque vamos a modificar
// sus valores
let mut context = Context::new();
// Ahora agregamos el {{nombre}} que espera nuestro template
// para ello el key debe coincidir con lo especificado en el template
// en este caso el valor que se pasa a "posts" son todos los Posts que seran serializados por
// Serde
let posts: Vec<Post> = obtener_todos_los_posts();
context.insert("posts", &posts);
// Ahora hacemos el render de nuestro template, pasando los valores
// del contexto para ser reemplazados al momento del render.
let respuesta_html = tera.render("blog/posts.html", &context).unwrap();
HttpResponse::Ok().body(respuesta_html)
}
// Esta es la forma de pasar un parametro por la ruta del url, en la definicion
// de la funcion, es importante poder definir el tipo de los parametros
#[get("/{id}")]
async fn post(tera: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse, MiError> {
let mut context = Context::new();
// el parametro "path" contiene todos los parametros pasados por url, en
// este especifico caso solo es uno de tipo String, si hubierann mas, entonces
// habria que especificar cada tipo o crear una estructura que contenga esos
// parametros, puedes aprender mas de parametros de ruta en la documentacion:
// https://actix.rs/docs/extractors#path
let id = path.into_inner(); // obteniendo el id
if let Some(post) = obtener_post(&id) {
// Si encuentra un post verificamos si tiene publicado = true
if post.publicado {
context.insert("post", &post);
let respuesta_html = tera.render("blog/post.html", &context).unwrap();
Ok(HttpResponse::Ok().body(respuesta_html))
} else {
// Si no esta publicado todavia, entonces regresaremos un error 404,
Err(MiError::PaginaNoEncontrada)
}
} else {
// En este caso si o encuentra el post, retornaremos un error 404 (Not Found)
Err(MiError::PaginaNoEncontrada)
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
// Creamos una configuracion de Tera, en la cual se especificamos
// que todos los templates se encuetran en la carpeta "templates"
// en la raiz del proyecto y que ademas dicha carpeta puede contener sub carpetas
let tera: Tera = Tera::new("templates/**/*").unwrap();
App::new()
.service(leer_archivo_estatico)
// Aca agregamos al "State" nuestra configuracio de tera
// para agregar al State, utilizamos el metodo .app_data
// al cual le pasamos de parametro un web::Data para poder
// manejar de forma segura la memoria junto con la configuracion
// de tera
.app_data(web::Data::new(tera))
.service(index)
.service(post)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Del código anterior lo importante a resaltar son estas líneas, la primera es la importación del módulo de manejo de errores:
mod manejo_errores;
use manejo_errores::MiError;
Ahora, veamos los cambios hechos en el método que busca el post por id y devuelve un resultado:
#[get("/{id}")]
async fn post(tera: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse, MiError> {
let mut context = Context::new();
// el parametro "path" contiene todos los parametros pasados por url, en
// este especifico caso solo es uno de tipo String, si hubierann mas, entonces
// habria que especificar cada tipo o crear una estructura que contenga esos
// parametros, puedes aprender mas de parametros de ruta en la documentacion:
// https://actix.rs/docs/extractors#path
let id = path.into_inner(); // obteniendo el id
if let Some(post) = obtener_post(&id) {
// Si encuentra un post verificamos si tiene publicado = true
if post.publicado {
context.insert("post", &post);
let respuesta_html = tera.render("blog/post.html", &context).unwrap();
Ok(HttpResponse::Ok().body(respuesta_html))
} else {
// Si no esta publicado todavia, entonces regresaremos un error 404,
Err(MiError::PaginaNoEncontrada)
}
} else {
// En este caso si o encuentra el post, retornaremos un error 404 (Not Found)
Err(MiError::PaginaNoEncontrada)
}
}
Lo primero a resaltar es la definición del método:
async fn post(tera: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse, MiError> {
Anteriormente, se retornaba un HttpResponse, pero ahora se retorna:
Result<HttpResponse, MiError>
Quiere decir que si hay un éxito, entonces se retornará un HttpResponse, pero si hay un error, entonces el error será de tipo MiError, que ya lo tenemos configurado para responder un contenido HTML también 😉
Lo otro a notar es que ahora cuando hay un éxito, lo debemos encapsular en un Ok() ya que estamos retornando un "Result"
Ok(HttpResponse::Ok().body(respuesta_html))
Pero lo más importante de este artículo sobre errores con actix-web es que ahora los errores se retornan de forma adecuada, por ejemplo cuando no se encuentran los posts, en lugar de retornar un HttpResponse, regresamos un error:
if let Some(post) = obtener_post(&id) {
// Si encuentra un post verificamos si tiene publicado = true
if post.publicado {
context.insert("post", &post);
let respuesta_html = tera.render("blog/post.html", &context).unwrap();
Ok(HttpResponse::Ok().body(respuesta_html))
} else {
// Si no esta publicado todavia, entonces regresaremos un error 404,
Err(MiError::PaginaNoEncontrada)
}
} else {
// En este caso si o encuentra el post, retornaremos un error 404 (Not Found)
Err(MiError::PaginaNoEncontrada)
}
Con la línea:
Err(MiError::PaginaNoEncontrada)
Indicamos que estamos regresando un error del enum:
MiError::PaginaNoEncontrada
Que recuerda, devolverá un status code 404
Probemos ahora nuestro código! 😎
En una terminal ejecuta el comando:
cargo run
Verás algunos warnings que vamos a ignorar por el momento, ya que los resolveremos en un instante.
Si ahora ingresas a la url desde tu navegador:
http://localhost:8080
Podrás ver la página inicial con todos los posts y si das click a los títulos de los posts podrás navegar sin problema, sin embargo, ahora probemos un post que no exista, en tu navegador coloca la siguiente url:
http://localhost:8080/no-existe
Verás el siguiente mensaje:
Si lo comparas con el mensaje de error mostrado en la primera imagen de este artículo, hay mucha diferencia, por ejemplo, ahora sí es un contenido HTML y siempre mantiene el status code 404, la otra diferencia es que ahora el método para mostrar el contenido del post devolvió un error 😁
Aunque el error sigue siendo bastante "feo" 🤔
Pero, hemos visto que podemos modificar nuestro método error_response de nuestro manejador de errores para hacerlo un poco más amigable al usuario.
Creemos un nuevo template para mostrar un error de página no encontrado. No nos detendremos mucho en la explicación de los templates puesto que ya hemos visto como construirlos en:
Desarrollo Web Con Rust - State y Templates HTML
Organizar templates HTML con actix-web y tera
Dentro de la carpeta "templates" creemos una sub carpeta de nombre "errores" y dentro creemos un archivo "error.html", tus archivos deberían de verse así:
En nuestro ejemplo únicamente tendremos un archivo de error, pero tu puedes crear tantos templates de error como casos de error quisieras manejar 😉
Ahora dentro de templates/errores.error.html colocamos el siguiente HTML:
{% extends "base.html" %}
{% block titulo %}
Error
{% endblock %}
{% block contenido %}
<section class="jumbotron text-center">
<div class="container">
<h1 class="jumbotron-heading">{{mensaje}}</h1>
<p class="lead text-muted">Error {{status_code}}</p>
</div>
</section>
{% endblock %}
Y ahora modificaremos nuestro error_response para mostrar el template en lugar de solamente el texto. Un detalle importante a tener en cuenta es que desafortunadamente no podremos acceder al State de la aplicación desde error_response, por lo que tendremos que crear una nueva instancia de Tera, sin embargo, esto no será un inconveniente ya que los casos de errores son simplemente excepciones dentro del flujo normal de datos.
Modifica el código de src/manejo_errores.rs para que se vea de esta forma:
// src/manejo_errores.rs
use actix_web::{
error,
http::{header::ContentType, StatusCode},
HttpResponse,
};
use derive_more::{Display, Error};
use tera::{Context, Tera};
#[derive(Debug, Display, Error)]
pub enum MiError {
#[display(fmt = "No encontrado")]
PaginaNoEncontrada,
#[display(fmt = "Ha ocurrido un error, estamos investigando la causa")]
ErrorGeneral,
}
impl error::ResponseError for MiError {
fn error_response(&self) -> HttpResponse {
// Aca creamos la nueva instancia de tera solo para manejo de errores
let mut context = Context::new();
let tera: Tera = Tera::new("templates/**/*").unwrap();
// Agregamos el status code y el mensaje
context.insert("status_code", &self.status_code().to_string());
context.insert("mensaje", &self.to_string());
let render = tera.render("errores/error.html", &context).unwrap();
// Ahora mostramos el template
HttpResponse::build(self.status_code())
.insert_header(ContentType::html())
.body(render)
}
fn status_code(&self) -> StatusCode {
match *self {
MiError::PaginaNoEncontrada => StatusCode::NOT_FOUND,
MiError::ErrorGeneral => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
Como puedes ver en error_response, ahora se está creando una nueva instancia de tera y leyendo el template, al cual le pasamos el status code que recibimos y el mismo mensaje que definimos en el Enum.
Ahora volvamos a ejecutar nuestra aplicación web con:
cargo run
Si ingresas desde tu navegador a:
http://localhost:8080/no-existe
Verás un nuevo formato de mensaje de error y si ves el status code de la respuesta, te darás cuenta que sigue siendo un 404 como lo esperábamos 😃
Ahora solo agreguemos el error code que nos hace falta, para simular como que hemos tenido un error en la "base de datos", vamos a poner una condición en la cual si no tenemos posts entonces regresaremos un error general (status code 500). Modifica tu código en src/main.rs para que se vea así:
// src/main.rs
mod archivos_estaticos;
mod manejo_errores;
mod posts;
use actix_web::{get, web, App, HttpResponse, HttpServer, Result};
use archivos_estaticos::leer_archivo_estatico;
use manejo_errores::MiError;
use posts::{obtener_post, obtener_todos_los_posts, Post};
use tera::{Context, Tera};
// Es importante notar que para acceder al State que se guardo
// con .app_data, se coloca como parametro en la definicion del metodo
// el web::Data<T> donde T es el tipo de dato que pasamos en .app_data
// Lo genial es que si no necesitamos utilizar Tera, simplemente podemos
// quitarlo de la definicion, es decir, no es obligatorio poner en las
// definicionnes de las funciones todo lo que se ecuentre en el state
#[get("/")]
async fn index(tera: web::Data<Tera>) -> Result<HttpResponse, MiError> {
// Ahora creamos el Context para pasarle valores a nuestro template HTML
// es importante notar que le agregamos un "mut" porque vamos a modificar
// sus valores
let mut context = Context::new();
// Ahora agregamos el {{nombre}} que espera nuestro template
// para ello el key debe coincidir con lo especificado en el template
// en este caso el valor que se pasa a "posts" son todos los Posts que seran serializados por
// Serde
let posts: Vec<Post> = obtener_todos_los_posts();
if posts.len() == 0 {
return Err(MiError::ErrorGeneral);
}
context.insert("posts", &posts);
// Ahora hacemos el render de nuestro template, pasando los valores
// del contexto para ser reemplazados al momento del render.
let respuesta_html = tera.render("blog/posts.html", &context).unwrap();
Ok(HttpResponse::Ok().body(respuesta_html))
}
// Esta es la forma de pasar un parametro por la ruta del url, en la definicion
// de la funcion, es importante poder definir el tipo de los parametros
#[get("/{id}")]
async fn post(tera: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse, MiError> {
let mut context = Context::new();
// el parametro "path" contiene todos los parametros pasados por url, en
// este especifico caso solo es uno de tipo String, si hubierann mas, entonces
// habria que especificar cada tipo o crear una estructura que contenga esos
// parametros, puedes aprender mas de parametros de ruta en la documentacion:
// https://actix.rs/docs/extractors#path
let id = path.into_inner(); // obteniendo el id
if let Some(post) = obtener_post(&id) {
// Si encuentra un post verificamos si tiene publicado = true
if post.publicado {
context.insert("post", &post);
let respuesta_html = tera.render("blog/post.html", &context).unwrap();
Ok(HttpResponse::Ok().body(respuesta_html))
} else {
// Si no esta publicado todavia, entonces regresaremos un error 404,
Err(MiError::PaginaNoEncontrada)
}
} else {
// En este caso si o encuentra el post, retornaremos un error 404 (Not Found)
Err(MiError::PaginaNoEncontrada)
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
// Creamos una configuracion de Tera, en la cual se especificamos
// que todos los templates se encuetran en la carpeta "templates"
// en la raiz del proyecto y que ademas dicha carpeta puede contener sub carpetas
let tera: Tera = Tera::new("templates/**/*").unwrap();
App::new()
.service(leer_archivo_estatico)
// Aca agregamos al "State" nuestra configuracio de tera
// para agregar al State, utilizamos el metodo .app_data
// al cual le pasamos de parametro un web::Data para poder
// manejar de forma segura la memoria junto con la configuracion
// de tera
.app_data(web::Data::new(tera))
.service(index)
.service(post)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Ahora, si quitas todos tus posts en el archivo src/posts.rs (no te preocupes, no tienes que hacerlo si no quieres, solo mira la siguiente imagen 😋), corres la app y miras el resultado, verás el siguiente mensaje con un status code 500 tal cual queríamos:
Perfecto, ahora tenemos nuestros errores implementados, y también removimos el warning que estaba saliendo al momento de compilar.
En este ejemplo únicamente hemos creado dos errores, pero, nuevamente como te lo mencionaba, tu puedes crear tantos errores como creas conveniente.
Otro disclaimer. En este ejemplo hemos utilizado tera dentro de error_response, otra forma de hacerlo es hacer una redirección, eso se puede lograr con:
HttpResponse::build(StatusCode::FOUND)
.insert_header(("Location", "alguna pagina de error")
.finish()
Eso enviará al usuario a alguna página, lo único que debes tomar en cuenta es que esa redirección genera un HTTP status code final de 200, si eso no te afectará es una aproximación válida, sin embargo, ten en cuenta que no es una práctica recomendada. Fin del disclaimer 😇.
También hay otro detalle a tomar en cuenta, en nuestro ejemplo, nosotros mismos hemos controlado los casos de error, pero, ten en cuenta que es probable que tu app web con Rust pueda tener interacción con otras librerías como por ejemplo para conectarse a una base de datos, las cuales, tienen implementados sus propios errores, entonces nos tocaría a nosotros verificar si se disparan o no antes de un unwrap(), sin embargo, como lo mencionaba casi al principio del artículo, ese tipo de errores se puede manejar más fácilmente con otros crates como thiserror, pero eso lo veremos en otro artículo 😉.
Ahora hemos aprendido los conceptos básicos para el manejo de errores con actix-web. Recuerda que puedes ver el código completo del ejemplo en este repositorio.
Si esta publicación te ha sido de utilidad, compártela con tus amigos y en las redes sociales. No te olvides de seguirme en X (twitter) ni de suscribirte al canal de youtube.
Si esta información te ha sido de utilidad y te gustaría apoyar a la creación de más artículos y contenidos puedes convertirte en mecenas desde patreon te estaré sumamente agradecido 🦀
println!("Hasta la próxima!");