טיפול בשגיאות

כדי לספק חוויית משתמש עקבית יותר, כדאי לפרש שגיאות ולהגיב להן באופן יזום. בין אם אתם מפתחים תהליכי עבודה אוטומטיים בענן או יוצרים אינטראקציה עם ממשקי 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);

דוגמת קוד: פונקציה ראשית (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 כוללים פרטים נוספים על שגיאות כשבקשות נכשלות. כדי לעזור בפתרון בעיות, ספריות הלקוח של Rust כוללות את הפרטים האלה כשמעצבים שגיאות באמצעות std::fmt::Display. אתם יכולים לבדוק את הפרטים האלה ולשנות את אופן הפעולה של האפליקציה בהתאם.

רק שגיאות שהשירות מחזיר מכילות מידע מפורט. ספריות הלקוח מחזירות enum‏ 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 {

כמו שצוין קודם, ספריות הלקוח מחזירות enum עם סוגים שונים של פרטי שגיאה 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 , הבקשה משתמשת במזהה Uniform Resource Identifier‏ (URI) כדי לציין משאב. חלק מה-RPC מתאימים לכמה 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;

הסבר על תבניות

הודעת השגיאה של שגיאת binding כוללת מחרוזות תבנית שמציגות ערכים אפשריים לשדות הבקשה. רוב מחרוזות התבניות כוללות את התווים * ו-** כתווים כלליים להתאמה לערכי השדות.

תו כללי יחיד לחיפוש

התו הכללי לחיפוש * לבדו מייצג מחרוזת לא ריקה ללא /. אפשר לחשוב על זה כעל הביטוי הרגולרי [^/]+.

הנה כמה דוגמאות:

תבנית קלט יש התאמה?
* 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");

המאמרים הבאים