錯誤處理

主動解讀並回應錯誤,提供更一致的使用者體驗。無論您是開發自動化雲端工作流程,還是與遠端 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. 請嘗試再次新增 Secret 版本。這次,如果發生任何錯誤,請傳回錯誤:

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

程式碼範例:主要函式 (sample)

本範例的完整程式碼分為三部分:主要協調函式 (sample),以及兩個輔助方法 (update_attempt 和 create_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 服務會在要求失敗時提供額外的錯誤詳細資料。為協助排解問題,使用 std::fmt::Display 格式化錯誤時,Rust 用戶端程式庫會提供這些詳細資料。您可以檢查這些詳細資料,並據此變更應用程式行為。

只有服務傳回的錯誤包含詳細資訊。用戶端程式庫會傳回 StatusDetails 列舉,其中包含不同類型的錯誤詳細資料。

擷取錯誤詳細資料

這個範例會刻意將錯誤要求傳送至 Cloud Natural Language API,並檢查產生的錯誤。

  1. 建立用戶端: <0x0A

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

上述範例錯誤是因為範例嘗試擷取資源詳細資料,但未提供資源名稱。具體來說,GetSecretRequest 的 name 欄位為必填,但範例未設定:

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");

後續步驟