了解 API 错误

本指南介绍了 Data Manager API 如何处理和传达错误。 了解 API 错误的结构和含义对于构建能够妥善处理问题(从无效输入到临时服务不可用)的强大应用至关重要。

Data Manager API 遵循标准的 Google API 错误模型,该模型基于 gRPC 状态代码。导致错误的每个 API 响应都包含一个 Status 对象,其中包含:

  • 数字错误代码。
  • 错误消息。
  • 可选,其他错误详情。

规范错误代码

Data Manager API 使用一组由 gRPC 和 HTTP 定义的规范错误代码。这些代码可简要指示错误类型。 您应始终先检查此代码,以了解问题的根本性质。

如需详细了解这些代码,请参阅 API 设计指南 - 错误代码。

快速失败模型

Data Manager API 采用快速失败模型。如果请求包含结构错误,或者任何记录未能通过必填字段验证,则整个请求都会失败,并且 API 不会处理该请求中的任何数据。

与部分失败模型的比较

快速失败模型不同于其他一些 Google API(例如 Google Ads API 和 Campaign Manager 360 API)中的部分失败模型。在部分失败模型中,即使某些记录存在错误,请求也会成功,并且响应会包含失败记录的错误详细信息。

虽然部分失败可能很方便,但它会带来重大风险,因为部分失败模型不会主动提醒您出现错误,您必须明确检查每个响应中是否存在错误。这可能会掩盖重要问题,因为即使 API 拒绝了请求中的许多甚至所有记录,请求也会成功。如果请求中的很大一部分记录存在错误,但您没有检查响应,则可能完全不知道数据存在普遍问题,直到几天或几周后,当累积结果与您的预期不符时,您才会发现这些问题。

快速失败模型可避免这些陷阱,它会在数据或集成出现问题时立即提醒您,以便您采取适当的措施。

使用 validateOnly 检查快速失败错误

大多数提取和内容移除要求都支持 validateOnly 字段。将 validateOnly 设置为 true 时,Data Manager API 会运行与常规请求相同的基本验证检查,但不会提取或移除任何数据。

  • 如果请求有错误,则会失败,并返回与常规请求相同的错误响应。
  • 如果请求通过验证,则会成功。响应包含可选字段的任何 fieldWarnings,就像常规请求一样。

使用 validateOnly 来执行以下操作:

  • 测试新的或更新后的集成,而不会影响您的实时数据。
  • 在重新发送请求之前,请确认修正措施已解决错误。

处理错误

当请求失败时,请按以下步骤操作:

  1. 查看错误代码,找出错误类型。

    • 如果您使用 gRPC,则错误代码位于 Status 的 code 字段中。如果您使用客户端库,它可能会抛出与错误代码对应的特定类型的异常。例如,如果错误代码为 INVALID_ARGUMENT,则 Java 版客户端库会抛出 com.google.api.gax.rpc.InvalidArgumentException。
    • 如果您使用 REST,则错误代码位于 error.status 的错误响应中,相应的 HTTP 状态位于 error.code。
  2. 检查错误代码的标准详细信息载荷。标准详细信息载荷是一组来自 Google API 的错误消息。它们以结构化且一致的方式为您提供错误详细信息。Data Manager API 的每个错误都可能有多条标准详细信息载荷消息。Data Manager API 客户端库提供了辅助方法,可从错误中获取标准详细信息载荷。

    无论错误代码是什么,我们都建议您检查并记录 ErrorInfo、RequestInfo、Help 和 LocalizedMessage 载荷。

    • ErrorInfo 包含的信息可能不在其他载荷中。
    • RequestInfo 包含请求 ID,如果您需要与支持团队联系,该 ID 会很有帮助。
    • Help 和 LocalizedMessage 包含链接和其他详细信息,可帮助您解决错误。

    此外,BadRequest 载荷对于 INVALID_ARGUMENT 错误很有用,因为它提供了有关哪些字段导致了错误的信息。

提取警告

Data Manager API 会尽可能多地接受提取请求。 如果您添加了非必需数据,则这些字段的验证失败不会导致请求失败。例如,如果购物车商品缺少商家商品 ID,API 会处理请求的其余部分并返回警告。

成功的提取响应(HTTP 状态代码 200)会在 fieldWarnings 列表中包含这些警告。每个条目都是一个 FieldWarning 对象,包含以下字段:

field

请求中字段的位置,采用蛇形命名法路径语法。

如果路径指向列表中的某个项(repeated 字段),则其索引会显示在列表名称后面的方括号 ([...]) 中。

例如,events.events[0].cart_data.items[0].merchant_product_id 标识与请求中第一个事件的购物车数据中的第一项相关的警告。

description

说明所提供的值导致警告的原因。

reason

用于标识警告类型的 WarningReason 枚举值。

FieldWarning 示例

以下是成功提取请求的示例响应,其中包含一条警告,因为其中一个购物车商品缺少商家商品 ID。

{
  "requestId": "126365e1-16d0-4c81-9de9-f362711e250a",
  "fieldWarnings": [
    {
      "field": "events.events[0].cart_data.items[0].merchant_product_id",
      "description": "The merchant product ID is missing in the cart item.",
      "reason": "WARNING_REASON_CART_DATA_ITEM_MERCHANT_PRODUCT_ID_MISSING"
    }
  ]
}

标准详细信息载荷

Data Manager API 最常见的标准详细信息载荷如下:

BadRequest

当请求失败并显示 INVALID_ARGUMENT(HTTP 状态代码 400)时,请检查 BadRequest 载荷。

BadRequest 消息表示请求包含值不正确的字段,或者缺少必填字段的值。查看 BadRequest 中的 field_violations 列表,找出哪些字段存在错误。每个 field_violations 条目都包含可帮助您修正错误的信息:

field

请求中字段的位置,采用蛇形命名法路径语法。

如果路径指向列表(repeated 字段)中的项,则其索引会显示在列表名称后面的方括号 ([...]) 中。

例如,destinations[0].operating_account.account_id 是 destinations 列表中第一项的 operating_account 中的 account_id。

description

说明此值导致错误的原因。

reason

ErrorReason 枚举,例如 INVALID_HEX_ENCODING 或 INVALID_CURRENCY_CODE。

BadRequest的示例

以下是包含 BadRequest 消息的 INVALID_ARGUMENT 错误的示例响应。field_violations 显示错误是 accountId 不是数字。field 值 destinations[0].login_account.account_id 表示,存在字段违规的 accountId 位于 destinations 列表中第一项的 login_account 中。

{
  "error": {
    "code": 400,
    "message": "There was a problem with the request.",
    "status": "INVALID_ARGUMENT",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
        "reason": "INVALID_ARGUMENT",
        "domain": "datamanager.googleapis.com",
        "metadata": {
          "requestId": "t-a8896317-069f-4198-afed-182a3872a660"
        }
      },
      {
        "@type": "type.googleapis.com/google.rpc.RequestInfo",
        "requestId": "t-a8896317-069f-4198-afed-182a3872a660"
      },
      {
        "@type": "type.googleapis.com/google.rpc.BadRequest",
        "fieldViolations": [
          {
            "field": "destinations[0].login_account.account_id",
            "description": "String is not a valid number.",
            "reason": "INVALID_NUMBER_FORMAT"
          }
        ]
      }
    ]
  }
}

以下是来自 INVALID_ARGUMENT 错误(带有 BadRequest 消息)的另一个示例响应。在这种情况下,field_violations 列表会显示两个错误:

  1. 第一个 event 在事件的第二个用户标识符上具有未经过十六进制编码的值。

  2. 第二个 event 的值在事件的第三个用户标识符上未进行十六进制编码。

{
  "error": {
    "code": 400,
    "message": "There was a problem with the request.",
    "status": "INVALID_ARGUMENT",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
        "reason": "INVALID_ARGUMENT",
        "domain": "datamanager.googleapis.com",
        "metadata": {
          "requestId": "t-6bc8fb83-d648-4942-9c49-2604276638d8"
        }
      },
      {
        "@type": "type.googleapis.com/google.rpc.RequestInfo",
        "requestId": "t-6bc8fb83-d648-4942-9c49-2604276638d8"
      },
      {
        "@type": "type.googleapis.com/google.rpc.BadRequest",
        "fieldViolations": [
          {
            "field": "events.events[0].user_data.user_identifiers[1]",
            "description": "The HEX encoded value is malformed.",
            "reason": "INVALID_HEX_ENCODING"
          },
          {
            "field": "events.events[1].user_data.user_identifiers[2]",
            "description": "The HEX encoded value is malformed.",
            "reason": "INVALID_HEX_ENCODING"
          }
        ]
      }
    ]
  }
}

RequestInfo

每当请求失败时,检查 RequestInfo 载荷。RequestInfo 包含可唯一标识 API 请求的 request_id。

{
  "@type": "type.googleapis.com/google.rpc.RequestInfo",
  "requestId": "t-4490c640-dc5d-4c28-91c1-04a1cae0f49f"
}

记录错误或与支持团队联系时,请务必提供请求 ID,以帮助诊断问题。

ErrorInfo

检查 ErrorInfo 消息,以检索其他标准详细信息载荷中可能未捕获的其他信息。ErrorInfo 载荷包含一个 metadata 映射,其中包含有关错误的信息。

例如,以下是因使用未启用 Data Manager API 的 Google Cloud 云项目的凭据而导致 PERMISSION_DENIED 失败的 ErrorInfo。ErrorInfo 提供有关错误的更多信息,例如:

  • 与请求关联的项目,位于 metadata.consumer 下。
  • 服务的名称,位于 metadata.serviceTitle 下。
  • metadata.activationUrl 下可启用该服务的网址。
{
  "error": {
    "code": 403,
    "message": "Data Manager API has not been used in project PROJECT_NUMBER before or it is disabled. Enable it by visiting https://console.cloud.google.com/apis/api/datamanager.googleapis.com/overview?project=PROJECT_NUMBER then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.",
    "status": "PERMISSION_DENIED",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
        "reason": "SERVICE_DISABLED",
        "domain": "googleapis.com",
        "metadata": {
          "consumer": "projects/PROJECT_NUMBER",
          "service": "datamanager.googleapis.com",
          "containerInfo": "PROJECT_NUMBER",
          "serviceTitle": "Data Manager API",
          "activationUrl": "https://console.cloud.google.com/apis/api/datamanager.googleapis.com/overview?project=PROJECT_NUMBER"
        }
      },
      ...
    ]
  }
}

配额和速率限制错误

当请求超出项目限制时,API 会返回 RESOURCE_EXHAUSTED 错误(HTTP 状态代码 429)。ErrorInfo 载荷在其 metadata 映射中提供有关超出哪个限制的详细信息:

consumer
与请求关联的 Google Cloud 项目,格式为 projects/PROJECT_NUMBER。
quota_limit
超出的配额限制的名称,例如 IngestionMutateRequestsPerMinutePerProject 或 IngestionMutateRequestsPerDayPerProject。您可以使用此值来确定应用是否超出了每分钟限额或每日限额。如需查看限制名称的完整列表,请参阅项目限制。
quota_location
强制执行配额的位置。对于 Data Manager API,此值始终为 global。
quota_metric
与限制关联的指标,例如 datamanager.googleapis.com/ingestion_mutate_requests。
service
服务的名称 datamanager.googleapis.com。

以下示例展示了当请求超出每分钟提取 mutate 请求限制时的 RESOURCE_EXHAUSTED 错误响应:

{
  "error": {
    "code": 429,
    "message": "Quota exceeded for quota metric 'Ingestion mutate requests' and limit 'Ingestion mutate requests per minute' of service 'datamanager.googleapis.com' for consumer 'project_number:PROJECT_NUMBER'.",
    "status": "RESOURCE_EXHAUSTED",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
        "reason": "RATE_LIMIT_EXCEEDED",
        "domain": "googleapis.com",
        "metadata": {
          "consumer": "projects/PROJECT_NUMBER",
          "quota_limit": "IngestionMutateRequestsPerMinutePerProject",
          "quota_location": "global",
          "quota_metric": "datamanager.googleapis.com/ingestion_mutate_requests",
          "service": "datamanager.googleapis.com"
        }
      }
    ]
  }
}

Help和LocalizedMessage

检查 Help 和 LocalizedMessage 载荷,以获取指向文档和已本地化出错提示的链接,这些链接可帮助您了解和修复错误。

例如,以下是因使用未启用 Data Manager API 的 Google Cloud 云项目的凭据而导致 PERMISSION_DENIED 失败的 Help 和 LocalizedMessage。Help 载荷显示可以启用服务的网址,LocalizedMessage 包含错误说明。

{
  "error": {
    "code": 403,
    "message": "Data Manager API has not been used in project PROJECT_NUMBER before or it is disabled. Enable it by visiting https://console.cloud.google.com/apis/api/datamanager.googleapis.com/overview?project=PROJECT_NUMBER then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.",
    "status": "PERMISSION_DENIED",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.LocalizedMessage",
        "locale": "en-US",
        "message": "Data Manager API has not been used in project PROJECT_NUMBER before or it is disabled. Enable it by visiting https://console.cloud.google.com/apis/api/datamanager.googleapis.com/overview?project=PROJECT_NUMBER then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry."
      },
      {
        "@type": "type.googleapis.com/google.rpc.Help",
        "links": [
          {
            "description": "Google API Console API activation",
            "url": "https://console.cloud.google.com/apis/api/datamanager.googleapis.com/overview?project=PROJECT_NUMBER"
          }
        ]
      },
      ...
    ]
  }
}

访问错误详情

如果您使用的是某个客户端库,请使用辅助方法获取标准详细信息载荷。

.NET

try {
    // Send API request
}
catch (Grpc.Core.RpcException rpcException)
{
    Console.WriteLine($"Exception encountered: {rpcException.Message}");
    var statusDetails =
        Google.Api.Gax.Grpc.RpcExceptionExtensions.GetAllStatusDetails(
            rpcException
        );
    foreach (var detail in statusDetails)
    {
        if (detail is Google.Rpc.BadRequest)
        {
            Google.Rpc.BadRequest badRequest = (Google.Rpc.BadRequest)detail;
            foreach (
                BadRequest.Types.FieldViolation? fieldViolation in badRequest.FieldViolations
            )
            {
                // Access attributes such as fieldViolation!.Reason and fieldViolation!.Field
            }
        }
        else if (detail is Google.Rpc.RequestInfo)
        {
            Google.Rpc.RequestInfo requestInfo = (Google.Rpc.RequestInfo)detail;
            string requestId = requestInfo.RequestId;
            // Log the requestId...
        }
        else if (detail is Google.Rpc.ErrorInfo)
        {
            Google.Rpc.ErrorInfo errorInfo = (Google.Rpc.ErrorInfo)detail;
            // Log the errorInfo.Reason and errorInfo.Metadata...

            // If handling a rate limit error, check the exceeded quota limit:
            if (errorInfo.Reason == "RATE_LIMIT_EXCEEDED" &&
                errorInfo.Metadata.TryGetValue("quota_limit", out string quotaLimit))
            {
                // Inspect quotaLimit to determine whether it is a per-minute
                // or daily limit (for example,
                // IngestionMutateRequestsPerMinutePerProject).
            }

            // Log the details in the 'Metadata' map...
            foreach (
                KeyValuePair<String, String> metadataEntry in errorInfo.Metadata
            )
            {
                // Log the metadataEntry.Key and metadataEntry.Value...
            }
        }
        else
        {
            // ...
        }
    }
}

Java

try {
  // Send API request
} catch (com.google.api.gax.rpc.InvalidArgumentException invalidArgumentException) {
  // Gets the standard BadRequest payload from the exception.
  BadRequest badRequest = invalidArgumentException.getErrorDetails().getBadRequest();
  for (int i = 0; i < badRequest.getFieldViolationsCount(); i++) {
    FieldViolation fieldViolation = badRequest.getFieldViolations(i);
    // Access attributes such as fieldViolation.getField() and fieldViolation.getReason()
  }

  // Gets the standard RequestInfo payload from the exception.
  RequestInfo requestInfo = invalidArgumentException.getErrorDetails().getRequestInfo();
  if (requestInfo != null) {
    String requestId = requestInfo.getRequestId();
    // Log the requestId...
  }
} catch (com.google.api.gax.rpc.ApiException apiException) {
  // Fallback exception handler for other types of ApiException.

  // Gets the standard ErrorInfo payload from the exception.
  ErrorInfo errorInfo = apiException.getErrorDetails().getErrorInfo();
  // Log the 'reason' and 'domain'...

  // If handling a rate limit error, check the exceeded quota limit:
  if (errorInfo != null && "RATE_LIMIT_EXCEEDED".equals(errorInfo.getReason())) {
    String quotaLimit = errorInfo.getMetadataMap().get("quota_limit");
    // Inspect quotaLimit to determine whether it is a per-minute
    // or daily limit (for example,
    // IngestionMutateRequestsPerMinutePerProject).
  }

  // Log the details in the 'metadata' map...
  for (Entry<String, String> metadataEntry : errorInfo.getMetadataMap().entrySet()) {
    // Log the metadataEntry key and value...
  }

  // Gets the standard RequestInfo payload from the exception.
  RequestInfo requestInfo = apiException.getErrorDetails().getRequestInfo();
  if (requestInfo != null) {
    String requestId = requestInfo.getRequestId();
    // Log the requestId...
  }
  ...
}

处理 gRPC 的标头大小错误

当提取或内容移除要求中的大量记录未能通过验证时,Data Manager API 会在响应的 BadRequest 载荷中返回所有错误。

在 gRPC 中,服务会在 HTTP/2 响应标头(元数据)中返回这些错误详细信息。默认情况下,gRPC 客户端库会将传入标头限制为最大大小(通常为 8-10 KB)。如果错误详情超出此限制,则客户端 HTTP/2 传输会在解析错误响应之前失败,并抛出如下异常:

Http2Exception: Header size exceeded max allowed size (10240)

由于客户端无法处理响应标头,因此会取消流(在服务器端日志中显示为 HTTP 状态 499 Client Closed Request),并且您的应用无法访问 BadRequest 错误详细信息。

如需解决此问题并检查完整的错误列表,请配置 gRPC 客户端,增加传入元数据的大小上限(例如 16 MB):

.NET

// Creates a client for the ingestion service. Increases the maximum metadata size to
// 16MB to prevent RESOURCE_EXHAUSTED errors when responses contain large error
// details.
IngestionServiceClient ingestionServiceClient = new IngestionServiceClientBuilder
{
    GrpcChannelOptions = GrpcChannelOptions.Empty.WithCustomOption(
        "grpc.max_metadata_size",
        16 * 1024 * 1024
    ),
}.Build();

Java

// Creates a client for the ingestion service. Increases the maximum metadata size to 16MB to
// prevent RESOURCE_EXHAUSTED errors when responses contain large error details.
InstantiatingGrpcChannelProvider provider =
    IngestionServiceSettings.defaultGrpcTransportProviderBuilder()
        .setMaxInboundMetadataSize(16 * 1024 * 1024)
        .build();
IngestionServiceSettings settings =
    IngestionServiceSettings.newBuilder().setTransportChannelProvider(provider).build();
try (IngestionServiceClient ingestionServiceClient = IngestionServiceClient.create(settings)) {
  // ...
}

节点

// Creates a client for the ingestion service. Increases the maximum metadata
// size to 16MB to prevent RESOURCE_EXHAUSTED errors when responses contain
// large error details.
const client = new IngestionServiceClient({
  'grpc.max_metadata_size': 16 * 1024 * 1024,
});

PHP

// Creates a client for the ingestion service. Increases the maximum metadata size to 16MB to
// prevent RESOURCE_EXHAUSTED errors when responses contain large error details.
$client = new IngestionServiceClient([
    'transportConfig' => [
        'grpc' => [
            'stubOpts' => [
                'grpc.max_metadata_size' => 16 * 1024 * 1024,
            ],
        ],
    ],
]);

Python

# Creates a client for the ingestion service. Increases the maximum
# metadata size to 16MB to prevent RESOURCE_EXHAUSTED errors when responses
# contain large error details.
channel = datamanager_v1.services.ingestion_service.transports.IngestionServiceGrpcTransport.create_channel(
    options=[("grpc.max_metadata_size", 16 * 1024 * 1024)]
)
transport = datamanager_v1.services.ingestion_service.transports.IngestionServiceGrpcTransport(
    channel=channel
)
client: datamanager_v1.IngestionServiceClient = (
    datamanager_v1.IngestionServiceClient(transport=transport)
)

错误处理最佳实践

如需构建弹性应用,请实施以下最佳实践。

发送前先验证
在构建或更改集成时,请发送将 validateOnly 设置为 true 的请求,以便在提取数据之前捕获快速失败错误。
检查错误详情
始终查找标准详细信息载荷之一,例如 BadRequest。每个标准详细信息载荷都包含可帮助您了解错误原因的信息。
区分客户端错误和服务器错误

确定错误是由实现(客户端)问题还是 API(服务器)问题引起的。

  • 客户端错误:INVALID_ARGUMENT、NOT_FOUND、PERMISSION_DENIED、FAILED_PRECONDITION、UNAUTHENTICATED 等代码。这些错误需要您更改请求或应用的状态/凭据。在解决问题之前,请勿重试请求。
  • 服务器错误:UNAVAILABLE、INTERNAL、DEADLINE_EXCEEDED、UNKNOWN 等代码。这些错误表明 API 服务存在暂时性问题。
实现重试策略

确定错误是否可以重试,并使用重试策略。

  • 仅针对暂时性服务器错误(例如 UNAVAILABLE、DEADLINE_EXCEEDED、INTERNAL、UNKNOWN 和 ABORTED)和每分钟速率限制(RESOURCE_EXHAUSTED 加上 RATE_LIMIT_EXCEEDED)进行重试。

  • 对于速率限制,请检查 ErrorInfo 中的 quota_limit:

    • 如果限制是每分钟(例如 IngestionMutateRequestsPerMinutePerProject),请暂停请求,并使用指数退避算法(带抖动)重试。

    • 如果限制是每日(例如 IngestionMutateRequestsPerDayPerProject),请勿立即重试。暂停处理,直到每日配额在美国太平洋时间零点重置。

  • 使用指数退避算法,在每次重试之间等待越来越长的时间。这有助于避免已经超负荷的服务不堪重负。例如,等待 1 秒,然后等待 2 秒,再等待 4 秒,依此类推,直至达到重试次数上限或总等待时间上限。

  • 在退避延迟时间中添加少量随机“抖动”,以防止许多客户端同时重试时出现“惊群”问题。

全面记录

记录完整的错误响应,包括所有标准详细信息载荷,尤其是请求 ID。这些信息对于调试和向 Google 支持团队报告问题(如有需要)至关重要。

提供用户反馈

根据标准详细信息载荷中的代码和消息,向应用的用户提供清晰且有用的反馈。例如,您可以说“缺少交易 ID”或“未找到目标账号 ID”,而不是只说“发生错误”。

遵循这些准则,您可以有效诊断和处理 Data Manager API 返回的错误,从而打造更稳定、更人性化的应用。