Manejo de errores en aplicaciones gRPC
Normalmente los métodos de cada controlador gRPC retornan GrpcResult<U>, que se resuelve en un tonic::Status cuando algo falla. Sin embargo, los servicios, repositorios y otros componentes suelen retornar errores de dominio propios de un módulo.
Por esto, Sword provee GrpcError, una macro para enums de errores. Esta macro genera la conversión de cada error a tonic::Status en función de los atributos definidos en cada variante.
Definiendo errores de dominio
use sword::grpc::*;
use thiserror::Error;
#[derive(Debug, Error, GrpcError)]
#[grpc_error(code = "internal")]
pub enum UserError {
#[error("Usuario no encontrado")]
#[grpc(code = "not_found", message = "Usuario no encontrado")]
NotFound,
#[error("El usuario ya existe")]
#[grpc(code = "already_exists", message = "El usuario ya existe")]
AlreadyExists,
}Como habrás visto, es necesario implementar thiserror::Error para que la macro GrpcError funcione correctamente. Esto permite que el error pueda ser propagado y transformado con más facilidad entre diferentes variantes.
Atributos disponibles
code: Código gRPC&stra retornar. Obligatorio.message: Mensaje a retornar en elStatus. Puede ser un literal o un campo de la variante.transparent: solo en variante sin campos, delega en otro tipoGrpcError.tracing: nivel de tracing para la variante. Ver Tracing.
Niveles válidos para tracing: trace, debug, info, warn, error.
Códigos gRPC válidos
Los valores aceptados por code siguen la referencia de códigos de status de gRPC.
Ver todos los códigos
okcancelledunknowninvalid_argumentdeadline_exceedednot_foundalready_existspermission_deniedresource_exhaustedfailed_preconditionabortedout_of_rangeunimplementedinternalunavailabledata_lossunauthenticated
Errores enriquecidos con GrpcStatus
GrpcError cubre la propagación de errores de dominio con ?, pero a veces necesitas una respuesta de error con más información: el estándar gRPC Richer Error Model define detalles estructurados que los clientes pueden leer (field violations, localized messages, retry hints, etc.). Para eso, Sword expone GrpcStatus, un builder de status con detalles encadenables.
Habilita la feature grpc-error-details:
[dependencies]
sword = { version = "x.y.z", features = ["grpc", "grpc-error-details"] }GrpcStatus ofrece un constructor por código de estado (GrpcStatus::InvalidArgument(), GrpcStatus::NotFound(), ...) y builders encadenables para los detalles. Se convierte a tonic::Status con .into() o .build():
use sword::grpc::*;
// ... assuming a request handler ...
Err(GrpcStatus::InvalidArgument()
.message("invalid request")
.bad_request("username", "username cannot be empty"))?Métodos de errores detallados disponibles
The bad_request() Builder
Añade una violación de campo para BadRequest.
pub fn bad_request(field: impl Into<String>, description: impl Into<String>) -> SelfThe localized_message() Builder
Fija un mensaje localizado (LocalizedMessage).
pub fn localized_message(locale: impl Into<String>, message: impl Into<String>) -> SelfThe error_info() Builder
Fija información del error (ErrorInfo).
pub fn error_info(domain: impl Into<String>, reason: impl Into<String>, metadata: HashMap<String, String>) -> SelfThe retry_after() Builder
Fija una pista de reintento (RetryInfo) con la espera dada.
pub fn retry_after(delay: std::time::Duration) -> SelfThe help() Builder
Añade un enlace de ayuda (Help).
pub fn help(description: impl Into<String>, url: impl Into<String>) -> SelfThe debug_info() Builder
Fija información de depuración (DebugInfo).
pub fn debug_info(stack_entries: impl Into<Vec<String>>, detail: impl Into<String>) -> SelfThe precondition_failure() Builder
Añade una violación de precondición (PreconditionFailure).
pub fn precondition_failure(violation_type: impl Into<String>, subject: impl Into<String>, description: impl Into<String>) -> SelfThe quota_failure() Builder
Añade una violación de cuota (QuotaFailure).
pub fn quota_failure(subject: impl Into<String>, description: impl Into<String>) -> SelfThe request_info() Builder
Fija información de la solicitud (RequestInfo).
pub fn request_info(request_id: impl Into<String>, serving_data: impl Into<String>) -> SelfThe resource_info() Builder
Fija información del recurso (ResourceInfo).
pub fn resource_info(resource_type: impl Into<String>, resource_name: impl Into<String>, owner: impl Into<String>, description: impl Into<String>) -> SelfLeyendo los detalles en el cliente
Puedes reconstruir el GrpcStatus desde el tonic::Status recibido con GrpcStatus::from_status(&status) y leer los detalles con StatusExt::get_error_details():
use sword::grpc::*;
let grpc_status = GrpcStatus::from_status(&status);
let details = status.get_error_details();
if let Some(bad_request) = details.bad_request() {
// ... inspeccionar field_violations
}¿GrpcError o GrpcStatus?
GrpcError convierte errores de dominio a Status para propagarlos con ? en cualquier método. GrpcStatus construye directamente una respuesta de error rica con detalles estructurados. Ambos coexisten: usa GrpcError para el flujo normal de errores y GrpcStatus cuando necesites adjuntar detalles estandarizados.
Tracing
Otro aspecto interesante de GrpcError es que permite habilitar tracing para cada variante mediante el atributo tracing = <nivel> (o el shorthand compatible #[tracing(nivel)]). Esto genera logs estructurados con información del error y los campos de la variante.
Por ejemplo, para UserError::Conflict con tracing = error:
#[error("Conflicto en {field}: {value}")]
#[grpc(code = "already_exists", message = "Conflicto en {field}: {value}", tracing = error)]
Conflict {
field: String,
value: String,
},La salida en consola se vería así:
ERROR gRPC error response error="Conflicto en username: Alice" error_type="Conflict" grpc_code="already_exists" field="username" value="Alice"Nivel por defecto
Cuando no se especifica tracing, el nivel se deriva del código gRPC, siguiendo la política auto del access logger:
| Código | Nivel |
|---|---|
ok | info |
| errores de cliente | warn |
| errores de servidor (el resto) | error |
Interpolación de mensajes
En el atributo message puedes referenciar campos de la variante con sintaxis {field}. Por ejemplo, en UserError:
#[derive(Debug, Error, GrpcError)]
#[grpc_error(code = "internal")]
pub enum UserError {
// ...
#[error("Conflicto en {field}: {value}")]
#[grpc(code = "already_exists", message = "Conflicto en {field}: {value}")]
Conflict {
field: String,
value: String,
},
}El compilador valida que los campos referenciados existan en la variante. No soportado en variantes tuple o unit.
Ejemplo completo
use crate::auth::AuthError;
use crate::users::UserError;
use sword::grpc::*;
use thiserror::Error;
#[derive(Debug, Error, GrpcError)]
#[grpc_error(code = "internal", tracing = error)]
pub enum AppError {
#[error("Auth error: {0}")]
#[grpc(transparent)]
Auth(#[from] AuthError),
#[error("User error: {0}")]
#[grpc(transparent)]
User(#[from] UserError),
#[error("Service unavailable")]
#[grpc(code = "unavailable", tracing = warn)]
Unavailable,
}use sword::grpc::*;
use thiserror::Error;
#[derive(Debug, Error, GrpcError)]
#[grpc_error(code = "internal")]
pub enum UserError {
#[error("Usuario no encontrado")]
#[grpc(code = "not_found", message = "Usuario no encontrado")]
NotFound,
#[error("El usuario ya existe")]
#[grpc(code = "already_exists", message = "El usuario ya existe")]
AlreadyExists,
#[error("Conflicto en {field}: {value}")]
#[grpc(code = "already_exists", message = "Conflicto en {field}: {value}", tracing = error)]
Conflict {
field: String,
value: String,
},
}use sword::grpc::*;
use thiserror::Error;
#[derive(Debug, Error, GrpcError)]
#[grpc_error(code = "unauthenticated")]
pub enum AuthError {
#[error("Token inválido")]
#[grpc(code = "unauthenticated", message = "Token inválido")]
InvalidToken,
}// AuthError::InvalidToken
Status::unauthenticated("Token inválido")
code = "unauthenticated" message = "Token inválido"
// UserError::NotFound
Status::not_found("Usuario no encontrado")
code = "not_found" message = "Usuario no encontrado"
// UserError::AlreadyExists
Status::already_exists("El usuario ya existe")
code = "already_exists" message = "El usuario ya existe"
// UserError::Conflict
Status::already_exists("Conflicto en username: Alice")
code = "already_exists" message = "Conflicto en username: Alice"
// AppError::Unavailable
Status::unavailable("Service unavailable")
code = "unavailable" message = "Service unavailable"Nota sobre transparent
#[grpc(transparent)] delega la conversión a tonic::Status en el error interno.
Es útil cuando tu variante envuelve otro tipo que ya implementa el flujo de conversión esperado.

