错误处理

通过主动解读错误并做出响应,提供更一致的用户体验。无论您是在开发自动云工作流还是与远程 API 交互,Rust 客户端库都提供了妥善处理错误的方法。本指南介绍了如何:

  • 处理错误: 检查错误类型,并根据服务状态代码(例如在遇到 NotFound 错误时创建缺失的资源)分支应用逻辑。
  • 检查错误详情: 提取并检查服务 Google Cloud 返回的丰富错误详情(例如错误 请求字段违规或配额失败),以排查 API 问题并动态调整运行时行为。
  • 解决绑定错误: 解读并解决因请求字段无效或缺失而导致的客户端 HTTP 绑定错误,确保您的请求顺利到达服务。

前提条件

本指南使用 Secret Manager 服务Cloud Natural Language API来演示错误处理。如需运行示例,请先执行以下操作:

  1. 启用 Secret Manager 服务
  2. 启用 Cloud Natural Language API
  3. 设置身份验证

依赖项

使用以下命令将所需的依赖项添加到 Cargo.toml 文件:

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

处理错误

Rust 客户端库可让您发现错误并做出响应。例如,您可以使用错误发现来分支行为:云服务中的常见模式是使用资源,就好像其容器存在一样,仅在遇到错误时才创建容器。如果容器通常存在,则此方法比在发出请求之前检查容器是否存在更高效。

以下示例演示了如何在尝试更新 Secret Manager 密文时捕获错误,并在密文尚不存在时创建它,从而处理缺失的资源。

  1. 尝试创建新的密文版本:

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

  2. 如果 update_attempt 成功,请输出成功结果并返回:

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

  3. 如果 update_attempt 失败,您必须消除失败原因的歧义。 请求可能会因多种原因而失败,例如连接断开或身份验证令牌出错。重试政策可以处理大多数此类错误。查找服务返回的错误:

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

  4. 查找与缺失的密文对应的错误:

    if status.code == Code::NotFound {

  5. 如果您遇到“未找到”错误 (Code::NotFound),请尝试创建密文:

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

  6. 尝试再次添加密文版本。这次,如果任何操作失败,则返回错误:

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

代码示例:main 函数 (sample)

此示例的完整代码分为三个部分:主编排函数 (sample),后跟其两个辅助方法 (update_attemptcreate_secret)。

sample 函数尝试向密文添加新版本。它会捕获客户端返回的错误,并检查该错误是否为 Code::NotFound 错误。如果未找到密文,该函数会创建最初缺失的密文并重试更新。

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)
        }
    }
}

代码示例:辅助方法 (update_attempt)

辅助方法 update_attempt 尝试添加密文版本,计算载荷数据的 CRC32c 校验和:

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)
}

代码示例:辅助方法 (create_secret)

辅助方法 create_secret 会创建缺失的密文并配置自定义重试政策:

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)
}

检查错误详情

某些 Google Cloud 服务会在请求失败时提供其他错误详情。 为了帮助进行问题排查,Rust 客户端库在使用 std::fmt::Display 格式化错误时会包含这些详情。您可以检查这些详情并相应地更改应用行为。

只有服务返回的错误包含详细信息。客户端 库会返回一个 StatusDetails 枚举,其中包含不同类型的错误详情。

提取错误详情

此示例有意向 Cloud Natural Language API 发送错误请求,并检查生成的错误。

  1. 创建客户端:

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

  2. 发送请求(在此示例中,缺少键字段):

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

  3. 使用标准 Rust 函数从结果中提取错误。 错误类型以易于用户理解的形式输出所有错误详情:

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

输出类似于以下内容:

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: {},
                    },
                ),
            ],
        },
    },
}

以编程方式检查错误详情

有时,您可能需要以编程方式检查错误详情。此示例会遍历数据结构并输出最相关的字段。

只有服务返回的错误包含详细信息,因此请先查询错误,查看其是否包含正确的错误类型。如果包含,您可以分解有关该错误的一些顶级信息:

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 错误:

StatusDetails::BadRequest(bad) => {

BadRequest 包含违规字段的列表。您可以遍历并输出每个字段的详情:

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

此类信息在开发期间可能很有用。 StatusDetails 的其他分支(例如 QuotaFailure)在运行时可能会用于限制应用。

预期输出

错误详情的输出类似于以下内容:

  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."

代码示例:检查错误详情

sample 函数有意向 Cloud Natural Language API 发送无效请求,以生成服务错误。然后,它会捕获错误并以编程方式提取 StatusDetails,以检查和输出特定的 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(())
}

解决绑定错误

使用 HTTP 向 Google Cloud 服务发送请求时,请求会使用 统一资源标识符 (URI) 来指定资源。某些 RPC 对应于多个 URI,请求的内容决定了使用哪个 URI。

客户端库会考虑所有可能的 URI,并且仅在没有 URI 可用时返回绑定错误。通常,当字段缺失或格式无效时,会发生这种情况。

如果您的请求未能为任何可能的 URI 提供包含有效格式的字段,您可能会遇到绑定错误:

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/*'

上述示例错误之所以发生,是因为该示例尝试检索资源的详细信息,但未提供其名称。具体而言,name 字段 在 GetSecretRequest 上是必需的,但该示例未设置该字段:

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

如何修正绑定错误

如需修正该错误,请设置必填字段,使其与错误消息中显示的其中一个模板匹配:

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

任一模板都允许客户端库向服务器发出请求。例如,以下代码与第一个模板匹配:

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

或者,以下代码与第二个模板匹配:

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

解读模板

绑定错误的错误消息包含模板字符串,这些字符串显示了请求字段的可能值。大多数模板字符串都包含 *** 作为 通配符来匹配字段值。

单通配符

* 通配符本身表示不带 / 的非空字符串。您可以将其视为正则表达式 [^/]+

下面是一些示例:

模板 输入 匹配?
* simple-string-123 true
projects/* projects/p true
projects/*/locations projects/p/locations true
projects/*/locations/* projects/p/locations/l true
* ""(空) false
* string/with/slashes false
projects/* projects/(空) false
projects/* projects/p/(额外的斜杠) false
projects/* projects/p/locations/l false
projects/*/locations projects/p false
projects/*/locations projects/p/locations/l false

双通配符

** 通配符不太常见,表示任何字符串。该字符串可以为 空,也可以包含任意数量的斜杠 (/)。您可以将其视为 正则表达式 .*

如果模板以 /** 结尾,则初始斜杠是可选的。

模板 输入 匹配?
** "" 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

检查绑定错误

如果您需要以编程方式检查错误,请检查该错误是否为绑定错误,并将其向下转换为 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");

后续步骤