Skip to content

Filtros de excepción ​

Cuando un handler, un guard, un pipe o un interceptor fallan, el error termina en la capa de excepciones. Esa capa decide qué respuesta recibe el cliente. Corvis trae un filtro por defecto que produce cuerpos JSON con el formato habitual, y te deja reemplazarlo por tus propios filtros donde lo necesites.

HttpException ​

En Corvis todos los errores del pipeline son una HttpException. Lleva un estado HTTP, un cuerpo JSON y, opcionalmente, la causa original.

rust
#[get(":id")]
fn find_one(&self, #[param("id", ParseIntPipe)] id: u32) -> HttpResult<Cat> {
    self.cats_service
        .find_one(id)
        .ok_or_else(|| NotFoundException::new(format!("Cat #{id} not found")))
}
json
{ "message": "Cat #42 not found", "error": "Not Found", "statusCode": 404 }

HttpResult<T> es un alias de Result<T, HttpException>. El operador ? funciona con cualquier error que se convierta en HttpException.

Construir una excepción ​

HttpException::new(cuerpo, estado) acepta como cuerpo un &str/String, un Vec de mensajes, un Value JSON o (); y como estado un u16, un StatusCode o una constante de HttpStatus:

rust
HttpException::new("Forbidden", 403);
// {"statusCode":403,"message":"Forbidden"}

HttpException::new(json!({ "status": 418, "error": "Soy una tetera" }), HttpStatus::I_AM_A_TEAPOT);
// el objeto se envía tal cual

Métodos útiles:

MétodoUso
get_status() / status()Estado como u16 / StatusCode
get_response()Cuerpo JSON (&Value)
message()Mensaje como texto
with_description("...")Reemplaza el campo error del cuerpo
with_cause(err)Adjunta la causa original (no se envía al cliente)
cause(), cause_is::<E>(), downcast_cause::<E>()Inspeccionan la causa
is_internal()true para errores inesperados (ver más abajo)
HttpException::from_anyhow(e) / into_anyhow()Conversión con anyhow::Error

Excepciones incluidas ​

Cada una tiene ::new(cuerpo) (que agrega error con la frase del estado) y ::empty(). Además, el tipo unitario se convierte en HttpException con .into() o con ?:

TipoEstado
BadRequestException400
UnauthorizedException401
ForbiddenException403
NotFoundException404
MethodNotAllowedException405
NotAcceptableException406
RequestTimeoutException408
ConflictException409
GoneException410
PreconditionFailedException412
PayloadTooLargeException413
UnsupportedMediaTypeException415
ImATeapotException418
MisdirectedException421
UnprocessableEntityException422
TooManyRequestsException429
InternalServerErrorException500
NotImplementedException501
BadGatewayException502
ServiceUnavailableException503
GatewayTimeoutException504
HttpVersionNotSupportedException505
rust
NotFoundException::empty();                    // {"message":"Not Found","statusCode":404}
NotFoundException::new("Cat not found");       // {"message":"Cat not found","error":"Not Found","statusCode":404}
BadRequestException::new("x").with_description("desc");
let token = extract_bearer_token(request).ok_or(UnauthorizedException)?;

Errores inesperados ​

Si un handler propaga con ? un error que no es una HttpException (un std::io::Error, un error de base de datos, un anyhow::Error), Corvis lo envuelve en una excepción interna:

json
{ "statusCode": 500, "message": "Internal server error" }

El cliente no ve el mensaje original. El filtro por defecto lo registra en el log con el contexto ExceptionsHandler, incluida la cadena de causas. Lo mismo ocurre con un panic dentro de un handler: se captura y se responde como 500.

HttpException::http_error(estado, mensaje) representa un error con estado propio que tampoco es intencional, como el 413 o el 415 del body parser: responde { statusCode, message } con ese estado, pero is_internal() es true.

Filtros propios ​

Un filtro implementa ExceptionFilter:

rust
#[async_trait]
pub trait ExceptionFilter: Send + Sync + 'static {
    fn catches(&self, _exception: &HttpException) -> bool { true }
    async fn catch(&self, exception: HttpException, host: &ArgumentsHost) -> Response;
}
  • catches decide qué excepciones atiende el filtro. Por defecto, todas.
  • catch arma la respuesta. host es el contexto de ejecución: host.switch_to_http().get_request() da la petición.
  • Si la Response que devuelves no tiene estado, se usa el de la excepción.

Este filtro de examples/cats-app agrega timestamp y path, y solo atiende excepciones intencionales:

rust
use corvis::prelude::*;

#[injectable]
pub struct HttpExceptionFilter;

#[async_trait]
impl ExceptionFilter for HttpExceptionFilter {
    fn catches(&self, exception: &HttpException) -> bool {
        // Solo las excepciones lanzadas a propósito, no los errores inesperados.
        !exception.is_internal()
    }

    async fn catch(&self, exception: HttpException, host: &ArgumentsHost) -> Response {
        let request = host.switch_to_http().get_request();
        let status = exception.get_status();
        let message = exception.get_response().get("message").cloned().unwrap_or(json!(exception.message()));
        Response::json(json!({
            "statusCode": status,
            "message": message,
            "timestamp": unix_timestamp(),
            "path": request.url(),
        }))
        .with_status(status)
    }
}

Como no hay jerarquía de clases de excepción, catches filtra por lo que sí tienes a mano: el estado (get_status() == 404), si es interna (is_internal()) o el tipo de la causa (cause_is::<sqlx::Error>(), por ejemplo).

Reutilizar el filtro por defecto ​

BaseExceptionFilter::handle(&exception) produce la respuesta estándar (y registra los errores internos). Úsalo para casos que tu filtro no quiere tratar de forma especial:

rust
#[async_trait]
impl ExceptionFilter for AuditFilter {
    async fn catch(&self, exception: HttpException, host: &ArgumentsHost) -> Response {
        if exception.get_status() == 403 {
            self.audit.record(host.request().path());
        }
        BaseExceptionFilter::handle(&exception)
    }
}

Aplicar filtros ​

AlcanceForma
Una ruta#[use_filters(HttpExceptionFilter)] sobre el método
Un controlador#[use_filters(HttpExceptionFilter)] debajo de #[controller]
Global, instanciaapp.use_global_filters(HttpExceptionFilter)
Global, con DIProvider::app_filter::<HttpExceptionFilter>() en providers de un módulo
rust
#[get("filtered/missing")]
#[use_filters(HttpExceptionFilter)]
async fn filtered(&self) -> HttpResult<()> {
    Err(NotFoundException.into())
}

En #[use_filters(...)] puedes pasar un tipo (se resuelve con inyección de dependencias en el módulo del controlador, así que debe ser #[injectable]) o una expresión que construya la instancia (#[use_filters(MiFiltro::new("x"))]).

use_global_filters recibe una instancia: no puede inyectar dependencias. Si el filtro las necesita, regístralo con Provider::app_filter::<T>():

rust
#[module(
    providers = [Provider::app_filter::<HttpExceptionFilter>()],
)]
pub struct AppModule;

Qué filtro responde ​

Para cada excepción, Corvis recorre los filtros en este orden y usa el primero cuyo catches devuelve true:

  1. Los filtros de la ruta.
  2. Los del controlador.
  3. Los globales: primero los registrados como provider (app_filter), después los de use_global_filters en el orden en que se agregaron.

Si ninguno la atiende, responde el filtro por defecto.

Las excepciones que se producen fuera de una ruta (en un middleware, el 404 de una ruta inexistente, un error del body parser) solo pasan por los filtros globales.

Gateways

Los filtros no se aplican a los gateways de WebSocket. Ver WebSockets: filtros de excepción.

Publicado bajo la licencia MIT.