Manejo de errores

Para brindar una experiencia del usuario más coherente, interpreta los errores de forma proactiva y responde a ellos. Ya sea que desarrolles flujos de trabajo automatizados en la nube o interactúes con APIs remotas, las bibliotecas cliente de Rust proporcionan formas de controlar los errores correctamente. En esta guía, se explica cómo hacer lo siguiente:

  • Controlar errores: Inspeccionar los tipos de errores y bifurcar la lógica de la aplicación según los códigos de estado del servicio, como crear un recurso faltante cuando se encuentra un error NotFound
  • Examinar los detalles del error: Extraer y examinar los detalles de errores enriquecidos, como las infracciones de campos de solicitudes incorrectas o las fallas de cuota, que devuelven los Google Cloud servicios para solucionar problemas de la API y ajustar de forma dinámica el comportamiento del tiempo de ejecución
  • Resolver errores de vinculación: Interpretar y resolver errores de vinculación HTTP del cliente causados por campos de solicitud no válidos o faltantes para garantizar que tus solicitudes lleguen al servicio sin problemas

Requisitos previos

En esta guía, se usan el servicio de Secret Manager y la API de Cloud Natural Language para demostrar el manejo de errores. Para ejecutar los ejemplos, primero haz lo siguiente:

  1. Habilita el servicio de Secret Manager.
  2. Habilita la API de Cloud Natural Language.
  3. Configura la autenticación.

Dependencias

Usa el siguiente comando para agregar las dependencias necesarias a tu archivo Cargo.toml:

cargo add google-cloud-secretmanager-v1 google-cloud-gax crc32c google-cloud-language-v2

Soluciona errores

Las bibliotecas cliente de Rust te permiten mostrar errores y reaccionar ante ellos. Por ejemplo, puedes usar la detección de errores para bifurcar el comportamiento: un patrón común en los servicios en la nube es usar un recurso como si existiera el contenedor, y solo crear el contenedor si encuentras un error. Si el contenedor suele existir, este enfoque es más eficiente que verificar si el contenedor existe antes de realizar la solicitud.

En el siguiente ejemplo, se muestra cómo controlar un recurso faltante mediante la captura del error cuando se intenta actualizar un secreto de Secret Manager y cómo crearlo si aún no existe.

  1. Intenta crear una versión secreta nueva:

    match update_attempt(&client, project_id, secret_id, data.clone()).await {

  2. Si update_attempt tiene éxito, imprime el resultado correcto y regresa:

    Ok(version) => {
        println!("new version is {}", version.name);
        Ok(version)
    }

  3. Si update_attempt falla, debes desambiguar la causa de la falla. La solicitud podría haber fallado por muchos motivos, como una conexión interrumpida o un error con los tokens de autenticación. Las políticas de reintento pueden abordar la mayoría de estos errores. Busca los errores que devuelve el servicio:

    Err(e) => {
        if let Some(status) = e.downcast_ref::<Error>().and_then(|e| e.status()) {

  4. Busca un error que corresponda a un secreto faltante:

    if status.code == Code::NotFound {

  5. Si encontraste un error "no encontrado" (Code::NotFound), intenta crear el secreto:

    let _ = create_secret(&client, project_id, secret_id).await?;

  6. Intenta agregar la versión secreta de nuevo. Esta vez, muestra un error si falla algo:

    let version = update_attempt(&client, project_id, secret_id, data).await?;
    println!("new version is {}", version.name);
    return Ok(version);

Muestra de código: función principal (sample)

El código completo de este ejemplo se divide en tres partes: la función de organización principal (sample) y sus dos métodos auxiliares (update_attempt y create_secret).

La función sample intenta agregar una versión nueva a un secreto. Captura el error que devuelve el cliente y verifica si es un error Code::NotFound. Si no se encuentra el secreto, la función crea el secreto que falta inicialmente y vuelve a intentar la actualización.

use google_cloud_gax::error::Error;
use google_cloud_gax::error::rpc::Code;
use google_cloud_secretmanager_v1::client::SecretManagerService;
use google_cloud_secretmanager_v1::model::SecretVersion;

pub async fn sample(
    project_id: &str,
    secret_id: &str,
    data: Vec<u8>,
) -> anyhow::Result<SecretVersion> {
    let client = SecretManagerService::builder().build().await?;

    match update_attempt(&client, project_id, secret_id, data.clone()).await {
        Ok(version) => {
            println!("new version is {}", version.name);
            Ok(version)
        }
        Err(e) => {
            if let Some(status) = e.downcast_ref::<Error>().and_then(|e| e.status()) {
                if status.code == Code::NotFound {
                    let _ = create_secret(&client, project_id, secret_id).await?;
                    let version = update_attempt(&client, project_id, secret_id, data).await?;
                    println!("new version is {}", version.name);
                    return Ok(version);
                }
            }
            Err(e)
        }
    }
}

Muestra de código: método auxiliar (update_attempt)

El método auxiliar update_attempt intenta agregar una versión secreta y calcula la suma de verificación CRC32c de los datos de la carga útil:

use google_cloud_secretmanager_v1::client::SecretManagerService;
use google_cloud_secretmanager_v1::model::{SecretPayload, SecretVersion};

pub(crate) async fn update_attempt(
    client: &SecretManagerService,
    project_id: &str,
    secret_id: &str,
    data: Vec<u8>,
) -> anyhow::Result<SecretVersion> {
    let checksum = crc32c::crc32c(&data) as i64;
    let version = client
        .add_secret_version()
        .set_parent(format!("projects/{project_id}/secrets/{secret_id}"))
        .set_payload(
            SecretPayload::new()
                .set_data(data)
                .set_data_crc32c(checksum),
        )
        .send()
        .await?;
    Ok(version)
}

Muestra de código: método auxiliar (create_secret)

El método auxiliar create_secret crea un secreto faltante y configura una política de reintento personalizada:

use google_cloud_gax::options::RequestOptionsBuilder;
use google_cloud_gax::retry_policy::AlwaysRetry;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use google_cloud_secretmanager_v1::client::SecretManagerService;
use google_cloud_secretmanager_v1::model::{Replication, Secret, replication};
use std::time::Duration;

pub async fn create_secret(
    client: &SecretManagerService,
    project_id: &str,
    secret_id: &str,
) -> anyhow::Result<Secret> {
    let secret = client
        .create_secret()
        .set_parent(format!("projects/{project_id}"))
        .with_retry_policy(
            AlwaysRetry
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(60)),
        )
        .set_secret_id(secret_id)
        .set_secret(
            Secret::new()
                .set_replication(Replication::new().set_replication(
                    replication::Replication::Automatic(replication::Automatic::new().into()),
                ))
                .set_labels([("integration-test", "true")]),
        )
        .send()
        .await?;
    Ok(secret)
}

Examina los detalles del error

Algunos Google Cloud servicios incluyen detalles de errores adicionales cuando fallan las solicitudes. Para ayudar con la solución de problemas, las bibliotecas cliente de Rust incluyen estos detalles cuando se formatean errores con std::fmt::Display. Puedes examinar estos detalles y cambiar el comportamiento de tu aplicación en consecuencia.

Solo los errores que devuelve el servicio contienen información detallada. Las bibliotecas cliente muestran una StatusDetails enumeración con los diferentes tipos de detalles de errores.

Extrae los detalles del error

En este ejemplo, se envía de forma intencional una solicitud incorrecta a la API de Cloud Natural Language y se examina el error resultante.

  1. Crea un cliente:

    let client = LanguageService::builder().build().await?;

  2. Envía una solicitud (en este ejemplo, falta un campo clave):

    let result = client
        .analyze_sentiment()
        .set_document(
            Document::new()
                // Missing document contents
                // .set_content("Hello World!")
                .set_type(Type::PlainText),
        )
        .send()
        .await;

  3. Extrae el error del resultado con las funciones estándar de Rust. El tipo de error imprime todos los detalles del error en un formato legible por humanos:

    let err = result.expect_err("the request should have failed");
    println!("\nrequest failed with error {err:#?}");

El resultado es similar a este:

request failed with error Error {
    kind: Service {
        status_code: Some(
            400,
        ),
        headers: Some(
            {
                "vary": "X-Origin",
                "vary": "Referer",
                "vary": "Origin,Accept-Encoding",
                "content-type": "application/json; charset=UTF-8",
                "date": "Sat, 24 May 2025 17:19:49 GMT",
                "server": "scaffolding on HTTPServer2",
                "x-xss-protection": "0",
                "x-frame-options": "SAMEORIGIN",
                "x-content-type-options": "nosniff",
                "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000",
                "accept-ranges": "none",
                "transfer-encoding": "chunked",
            },
        ),
        status: Status {
            code: InvalidArgument,
            message: "One of content, or gcs_content_uri must be set.",
            details: [
                BadRequest(
                    BadRequest {
                        field_violations: [
                            FieldViolation {
                                field: "document.content",
                                description: "Must have some text content to annotate.",
                                reason: "",
                                localized_message: None,
                                _unknown_fields: {},
                            },
                        ],
                        _unknown_fields: {},
                    },
                ),
            ],
        },
    },
}

Examina los detalles del error de forma programática

A veces, es posible que debas examinar los detalles del error de forma programática. En este ejemplo, se atraviesa la estructura de datos y se imprimen los campos más relevantes.

Solo los errores que devuelve el servicio contienen información detallada, por lo que primero consulta el error para ver si contiene el tipo de error correcto. Si lo hace, puedes desglosar información de nivel superior sobre el error:

if let Some(status) = err.status() {
    println!(
        "  status.code={}, status.message={}",
        status.code, status.message,
    );

Itera sobre los detalles:

for detail in status.details.iter() {
    match detail {

Como se mencionó anteriormente, las bibliotecas cliente muestran una StatusDetails enumeración con los diferentes tipos de detalles de errores. En este ejemplo, solo se examinan los errores BadRequest:

StatusDetails::BadRequest(bad) => {

Un BadRequest contiene una lista de campos que están en infracción. Puedes iterar e imprimir los detalles de cada uno:

for f in bad.field_violations.iter() {
    println!(
        "  the request field {} has a problem: \"{}\"",
        f.field, f.description
    );
}

Esa información puede ser útil durante el desarrollo. Otras ramas de StatusDetails, como QuotaFailure, pueden ser útiles en el tiempo de ejecución para limitar una aplicación.

Resultado esperado

El resultado de los detalles del error es similar al siguiente:

  status.code=400, status.message=One of content, or gcs_content_uri must be set., status.status=Some("INVALID_ARGUMENT")
  the request field document.content has a problem: "Must have some text content to annotate."

Código de muestra: Examina los detalles del error

La función sample envía una solicitud intencionalmente no válida a la API de Cloud Natural Language para generar un error de servicio. Luego, captura el error y extrae de forma programática el StatusDetails para inspeccionar e imprimir infracciones específicas del campo BadRequest.

use google_cloud_gax::error::rpc::StatusDetails;
use google_cloud_language_v2::client::LanguageService;
use google_cloud_language_v2::model::Document;
use google_cloud_language_v2::model::document::Type;

pub async fn sample() -> anyhow::Result<()> {
    let client = LanguageService::builder().build().await?;

    let result = client
        .analyze_sentiment()
        .set_document(
            Document::new()
                // Missing document contents
                // .set_content("Hello World!")
                .set_type(Type::PlainText),
        )
        .send()
        .await;

    let err = result.expect_err("the request should have failed");
    println!("\nrequest failed with error {err:#?}");

    if let Some(status) = err.status() {
        println!(
            "  status.code={}, status.message={}",
            status.code, status.message,
        );
        for detail in status.details.iter() {
            match detail {
                StatusDetails::BadRequest(bad) => {
                    for f in bad.field_violations.iter() {
                        println!(
                            "  the request field {} has a problem: \"{}\"",
                            f.field, f.description
                        );
                    }
                }
                _ => {
                    println!("  additional error details: {detail:?}");
                }
            }
        }
    }

    Ok(())
}

Resuelve errores de vinculación

Cuando se usa HTTP para enviar solicitudes a Google Cloud servicios, la solicitud usa un identificador de recursos uniforme (URI) para especificar un recurso. Algunas RPC corresponden a varios URIs, y el contenido de la solicitud determina qué URI se usa.

La biblioteca cliente considera todos los URIs posibles y solo muestra un error de vinculación si no funciona ningún URI. Por lo general, esto sucede cuando falta un campo o tiene un formato no válido.

Si tu solicitud no proporciona campos que contengan un formato válido para cualquier URI posible, es posible que encuentres un error de vinculación:

Error: cannot find a matching binding to send the request: at least one of the
conditions must be met: (1) field `name` needs to be set and match the template:
'projects/*/secrets/*' OR (2) field `name` needs to be set and match the
template: 'projects/*/locations/*/secrets/*'

El error del ejemplo anterior ocurrió porque el ejemplo intenta recuperar los detalles de un recurso sin proporcionar su nombre. En particular, el campo name field en un GetSecretRequest es obligatorio, pero el ejemplo no lo establece:

let secret = client
    .get_secret()
    //.set_name("projects/my-project/secrets/my-secret")
    .send()
    .await;

Cómo corregir errores de vinculación

Para corregir el error, configura el campo obligatorio de modo que coincida con una de las plantillas que se muestran en el mensaje de error:

  • 'projects/*/secrets/*'
  • 'projects/*/locations/*/secrets/*'

Cualquiera de las plantillas permite que la biblioteca cliente realice una solicitud al servidor. Por ejemplo, el siguiente código coincide con la primera plantilla:

let secret = client
    .get_secret()
    .set_name("projects/my-project/secrets/my-secret")
    .send()
    .await;

Como alternativa, el siguiente código coincide con la segunda plantilla:

let secret = client
    .get_secret()
    .set_name("projects/my-project/locations/us-central1/secrets/my-secret")
    .send()
    .await;

Interpretación de plantillas

El mensaje de error de un error de vinculación incluye cadenas de plantilla que muestran los valores posibles para los campos de solicitud. La mayoría de las cadenas de plantilla incluyen * y ** como comodines para que coincidan con los valores de los campos.

Comodín único

El comodín * por sí solo significa una cadena no vacía sin un /. Se puede considerar como la expresión regular [^/]+.

Estos son algunos ejemplos:

Plantilla Entrada ¿Alguna coincidencia?
* simple-string-123 true
projects/* projects/p true
projects/*/locations projects/p/locations true
projects/*/locations/* projects/p/locations/l true
* "" (vacío) false
* string/with/slashes false
projects/* projects/ (vacío) false
projects/* projects/p/ (barra adicional) false
projects/* projects/p/locations/l false
projects/*/locations projects/p false
projects/*/locations projects/p/locations/l false

Comodín doble

Menos común es el comodín **, que significa cualquier cadena. La cadena puede estar vacía o contener cualquier cantidad de barras (/). Se puede considerar como la expresión regular .*.

Cuando una plantilla termina en /**, la barra inicial es opcional.

Plantilla Entrada ¿Alguna coincidencia?
** "" true
** simple-string-123 true
** string/with/slashes true
projects/*/** projects/p true
projects/*/** projects/p/locations true
projects/*/** projects/p/locations/l true
projects/*/** locations/l false
projects/*/** projects//locations/l false

Inspecciona errores de vinculación

Si necesitas inspeccionar el error de forma programática, verifica si es un error de vinculación y conviértelo en un BindingError:

let secret = client
    .get_secret()
    //.set_name("projects/my-project/secrets/my-secret")
    .send()
    .await;

let e = secret.unwrap_err();
assert!(e.is_binding(), "{e:?}");
assert!(e.source().is_some(), "{e:?}");
let _ = e
    .source()
    .and_then(|e| e.downcast_ref::<BindingError>())
    .expect("should be a BindingError");

¿Qué sigue?