操作指南:处理错误

前言

本指南介绍与 C2C API 交互时的错误处理机制。 妥善处理 HTTP 错误是与任何第三方服务进行稳健集成的重要组成部分。

错误类型

集成中的错误可能源自各种不同来源,我们可以将其分为四大类:

  • **I/O 错误:**源于设备上的硬件操作,例如读取/写入操作失败
  • **应用程序错误:**由应用程序代码内部问题引起
  • **网络错误:**发生在网络堆栈中,并由您的网络库进行传递
  • **API 错误:**由 Frame.io 的后端服务生成

每种错误类别都需要特定的处理考量。 本指南主要关注 API 错误,不过我们也会涉及针对其他类别的通用策略。

API 错误的返回方式

Frame.io 的 API 通过两种主要机制来传递错误:

  • **状态代码:**指示问题性质的 HTTP 错误代码
  • **错误消息:**提供额外错误详细信息的负载内容,尤其是在多种错误条件共享同一状态代码时

错误状态代码

HTTP 状态代码是标准化的数字响应,用于传达 HTTP 请求的结果。 有关更多信息,请参阅 Mozilla 的 HTTP 状态代码文档或通过 HTTP Cats 获取更直观的方法。 每个 Frame.io API 端点都会指定一个预期的成功状态代码——通常为 200(成功)201(已创建)204(无内容)。 您可以通过检查这些具体的状态代码或确认代码在 200-299 范围内来验证请求是否成功。 状态代码大于 399 则表示出错。 大多数 API 错误会返回 4XX 代码 (400-499),表示客户端问题。 超出此范围的错误通常源于您的设备与我们服务之间的网络基础设施,但有一个显著的例外是 500(内部服务器错误),它表示我们服务器内部出现了意外问题。 同样,404(未找到)响应也可能由中间服务生成,而非来自我们的后端,尽管它属于 4XX 代码。

如果您遇到意外的状态代码,请通知我们的团队。

错误负载模式

Frame.io 以两种格式返回错误详情:simpledetailed。 您的错误处理逻辑应能够兼容这两种格式。

简单错误模式

以下是一个使用了错误 client_secret 的失败请求示例:

$curl -X POST https://api.frame.io/v2/auth/device/code \
> --include \
> --header 'x-client-version: 2.0.0' \
> --form 'client_id=Some-Client-ID' \
> --form 'client_secret=bad_secret' \
> --form 'scope=asset_create offline'

响应:

HTTP/2 400
...
{"error":"invalid_client"}

简单错误模式仅包含一个用于标识错误的字段。

详细错误模式

作为对比,以下是一个未经过正确授权的请求示例:

$curl -X POST https://api.frame.io/v2/devices/heartbeat \
> --header 'Authorization: Bearer bad-token' \
> --header 'x-client-version: 2.0.0' \
> | python -m json.tool

响应:

1{
2 "code": 409,
3 "errors": [
4 {
5 "code": 409,
6 "detail": "The channel you're uploading from is currently paused.",
7 "status": 409,
8 "title": "Channel Paused"
9 }
10 ],
11 "message": "Channel Paused"
12}

详细错误模式包含一个用于标识错误类型的消息字段。

确定错误类型

在处理 Frame.io 错误时,首先检查是否存在错误负载,如果不存在负载,则回退到 HTTP 状态代码。

以下是一个基本的错误处理实施示例:

Python
1# Dict of known error codes: native errors.
2ERROR_STATUS_MAP = {
3 429: SlowDownError,
4 ...
5}
6
7# Dict of known error messages: native errros.
8ERROR_MESSAGE_MAP = {
9 "Channel Paused": ChannelPausedError,
10 "invalid_client": InvalidClientError,
11 "slow_down": SlowDownError,
12 ...
13}
14
15def _c2c_extract_error_message(response):
16 """
17 Gets the error message from an error payload. Returns `None`
18 if an error payload is not found.
19 """
20
21 # Try to decode the payload, if it is not JSON return `None`
22 try:
23 payload = response.json()
24 except JSONDecodeError:
25 return None
26
27 # Try the simple error schema first.
28 message = payload.get("error", default=None)
29 if message is not None:
30 return message
31
32 # Now try the detailed schema. Return None if we do not find one.
33 return payload.get("message", default=None)
34
35def _c2c_error_type_from_response(response):
36 """
37 Converts a bad HTTP response into an error.
38 """
39 error_message = _c2c_extract_error_message(response)
40
41 # try to do a lookup of the error type by message.
42 error_type = ERROR_MESSAGE_MAP.get(error_message, default=None)
43 if error_type is not None:
44 return error_type()
45
46 # If not, try to do a lookup by error code.
47 error_type = ERROR_STATUS_MAP.get(response.status_code, default=None)
48 if error_type is not None:
49 return error_type()
50
51 # Otherwise we are going to return an `UnknownAPIError` to signal that we
52 # encoutnered an error from Frame.io's backend servers, but do not know the
53 # message and/or status code.
54 return UnknownAPIError(message=error_message)
55
56def raise_on_frameio_error(response, expected_status):
57 """
58 Raises a native error from an HTTP response if the response indicates an error
59 occured. Expected status should be the status we expect to get (200, 201, 204,
60 etc).
61 """
62
63 # If the status code is less than `400`, then it is not an error status code.
64 if response.status < 400:
65
66 # Check that the status code is the one we expected, otherwise raise an
67 # error.
68 if response.status != expected_status:
69 raise UnexpectedStatusError(
70 expected=expected_status, received=response.status
71 )
72
73 return None
74
75 # Otherwise convert and raise a native error.
76 raise _c2c_error_type_from_response(response)

此示例中引用的错误查找表在本指南末尾提供。

AWS 错误

上传文件分片时,您将直接与 AWS S3 交互,而 AWS S3 有其独立的错误格式。 有关详细信息,请参阅 AWS 常见错误文档。 作为一般规则,非致命性的 AWS 错误应至少重试一次。

AWS 错误以 XML 格式返回:

1<?xml version="1.0" encoding="UTF-8"?>
2<Error>
3 <Code>NoSuchKey</Code>
4 <Message>The resource you requested does not exist</Message>
5 <Resource>/mybucket/myfoto.jpg</Resource>
6 <RequestId>4442587FB7D0A2F9</RequestId>
7</Error>

其中 Code 元素标识错误类型。

重试错误

何时重试

本指南中的错误表指明了哪些 API 错误应进行重试。 对于来自 I/O 操作、网络库或 AWS 的非 API 错误,请考虑重试那些可能由瞬态条件导致的错误。 网络拥塞、临时服务器中断或数据包丢失等情况通常需要重试。 大多数网络库在请求耗时过长时会引发 TimeoutError,这是非常适合重试的情况。

如有疑问,重试一次

计算环境可能会遇到不可预测的问题。 即使对于看似致命的错误,通常也值得进行一次重试。 临时系统状态、硬件异常(例如宇宙射线导致的比特翻转)或罕见的内存状况,都可能导致看似致命的错误,而这些错误在第二次尝试时可能就会得以解决。 不过,有些错误不应重试。 例如,在创建资产时收到 409: CHANNEL PAUSED 响应,表示设备已暂停,不应进行上传。 这种状态是人为设定的,不太可能通过一次重试而改变。

指数退避

Frame.io 实施速率限制,超出这些限制会产生一个 429: Slow Down 错误或带有以下负载的 400 状态:

HTTP/2 400
{"error":"slow_down"}

当您收到这些响应时,请对重试实施指数退避。 计算延迟(以秒为单位)的推荐公式如下:

Python
1delay = min(2 ** attempt / 2, 32.0)

其中 attempt 从 0 开始。 这会产生 0.5 秒、1 秒、2 秒、4 秒、8 秒、16 秒、32 秒的延迟,之后的所有后续尝试都等待 32 秒。

退避抖动

我们建议在退避时间中添加随机性(抖动),以防止在多个设备从同一错误状况中恢复时出现请求同步。 这有助于缓解惊群效应,即故障后大量设备同时重试。 一个好的方法是添加一个介于 0 和计算延迟的一半之间的随机偏移量:math.rand(0, delay // 2)

虽然指数退避对于处理速率限制错误至关重要,但它通常也有利于处理网络和 I/O 故障。 这种方法能够在不增加重试所带来的额外负载的情况下,让临时资源限制得到缓解。

检测断连状态

当出现网络错误时,可能表明 Frame.io 无法访问,原因如下:

  • 您的本地网络中断
  • Frame.io 的服务遇到问题
  • 某个中间网络组件发生故障

检测这些状况非常重要。 当错误提示存在连接问题时,请实施一个监测任务来检查服务恢复情况,并将断连情况告知用户。

等待连接和授权

设计您的应用程序,避免在设备刷新授权、等待用户授权或无法访问 Frame.io 时发出不必要的请求。 这会减少网络开销并改善用户体验。

当您检测到断连状态时,请阻止所有 API 调用(除了对 https://api.frame.io/health 的调用)。 当出现连接问题时,启动一个后台任务来轮询运行状况端点,并阻止进一步的 API 调用,直到连接恢复。

同样,如果发生令牌过期,则阻止依赖授权的调用,直到颁发新的令牌。 如果令牌刷新失败,则提醒用户重新进行身份验证。

轮询连接状态时,请应用之前描述的同一指数退避方法。

请求超时

为不同类型的请求配置适当的超时值:

  • 默认:基本请求为 15 秒
  • 授权刷新:2 分钟,以适应潜在的后端处理时间
  • 文件分片上传:5 分钟,以适应传输较大数据时的慢速网络

重试处理程序示例

下面是一个展示带有指数退避的错误处理的伪代码实施:

Python
1# List of errors we know are fatal and should not be retried.
2FATAL_ERRORS = (
3 ChannelPausedError,
4 DevicesDisabledError,
5 ...
6)
7
8# List of errors we know should be retried more than once.
9RETRY_ERRORS = (
10 TimeoutError,
11 NotFoundError,
12 SlowDownError,
13 UnknownAPIError,
14 ...
15)
16
17# List of errors that could be the result of Frame.io being unreachable.
18DISCONNECTED_ERRORS = (
19 TimeoutError,
20 HttpClientError,
21 ...
22)
23
24def retry_with_backoff(next_handler):
25 """
26 Middleware for retrying errors with exponential backoff.
27 """
28
29 def retry_handler(call, retry_count):
30 """
31 Handler for retrying c2c API calls with exponential backoff.
32 """
33
34 error = None
35
36 # We will retry the call 8 times here, totalling 63.5 seconds +- ~32 seconds.
37 for attempt in range(start=1, stop=retry_count + 1):
38
39 # If we are attempting to reach an endpoint that requires authorization
40 # we should wait unil we have valid authorization before attempting
41 # a call. We need to do this each time in case our access_token
42 # expires between attempts.
43 C2C.wait_for_authorized(call)
44
45 # Likewise, we should wait until we are connected to Frame.io to attempt
46 # a call if we are not calling `https://api.frame.io/health`
47 C2C.wait_for_connected(call)
48
49 try:
50 # Return the result on a success.
51 return next_handler(call)
52 except FATAL_ERRORS as error:
53 # If we hit an error we know is fatal, raise the error without
54 # retrying it.
55 raise error
56
57 except RETRY_ERRORS as error:
58 # If we hit an error we know we should retry many times, continue,
59 # but notify our client if we think we may have been disconnected.
60 if type(error) in DISCONNECTED_ERRORS:
61 C2C.notify_disconnected()
62
63 except BaseException as error:
64 # Otherwise, do not retry the call more than once.
65 if attempt > 1:
66 raise error
67
68 # The delay for the next attempt should be no more than 32 seconds.
69 # This algorithm will go: 0.5s, 1s, 2s, 4s, 8s, 16s, 32s, 32s, ...
70 delay = min(2 ** attempt / 2, 32.0)
71
72 # Add some randomness (jitter) to the delay (up to half the value of
73 # the delay in either direction).
74 delay += math.random(-delay, delay) / 2
75
76 # Wait between retries
77 sleep(delay)
78
79 # If we have exhausted all retries,
80 raise error
81
82 return retry_handler

错误表

下表对 Frame.io API 错误进行了分类,并提供了处理指南。 以下是各列所代表的含义:

message:错误负载消息标识符 http code:HTTP 状态代码 error type:概念性错误类别(在描述部分中有详细说明)schema:错误负载格式(simpledetailedretry:推荐重试(yes 表示尝试多次,once 表示重试一次,no 表示致命错误)星号 (*) 表示特殊注意事项(在描述部分有详细说明)。

Frame.io 错误消息

消息错误类型HTTP 代码模式重试
”access_denied”AccessDenied401simple一次
”authorization_pending”AuthorizationPending400simple
”Channel Paused”ChannelPaused409simple
”expired_token”ExpiredToken400simple
”Invalid Argument”InvalidArgument422detailed
”invalid_client”InvalidClient400simple
”Invalid client version”InvalidClientVersion400simple
”invalid_grant”InvalidGrant400simple
”invalid_request”InvalidRequest400simple一次
”Not Authorized”UnauthorizedClient401detailed
”slow_down”SlowDown400simple
”unauthorized_client”UnauthorizedClient401simple是*

Frame.io 状态代码

HTTP 代码错误类型重试
400InvalidRequest一次
401UnauthorizedClient
422InvalidContentType
429SlowDown
500InternalServerError

AWS 错误

请参阅 AWS 文档获取详细说明。

错误重试
InternalError
OperationAborted
RequestTimeout
ServiceUnavailable
SlowDown
[所有其他错误]一次
解析类似的 AWS 错误

AWS 的 SlowDownServiceUnavailable 错误都表示请求速率问题,可以像处理 Frame.io 的 SlowDown 错误一样进行处理,实施指数退避。 同样,AWS 的 InternalError 在概念上对应于我们 API 中的 InternalServerError

说明

AccessDenied

当用户在设备配对过程中拒绝授权时返回。

AuthorizationPending

表示用户尚未输入设备配对代码。 请在设备代码响应中指定的时间间隔后继续轮询。

ChannelPaused

创建资产时设备通信已暂停。 请勿尝试重新上传此资产。

ExpiredToken

设备配对代码已过期。 请生成一个新代码并重新开始配对流程。

InternalServerError

表示出现意外的后端问题。 请重试一次,并将 500 错误报告给我们的团队进行调查。 请注意,某些已知问题在应该返回 InvalidRequest 时却返回了 500 错误:

  • 尝试上传到不存在的设备通道
  • 请求无效的自定义分片数量

InvalidArgument

负载参数包含无效值。 验证参数值是否符合 API 要求。

InvalidContentType

请求的 Content-Type 标头不受支持。 API 通常接受:

  • form/multipart(仅限授权端点)
  • application/x-www-form-urlencoded(所有端点)
  • application/json(非授权端点)

InvalidClient

提供的凭据(client_idclient_secret 等) 未被识别。 请验证您的集成凭据。

InvalidClientVersion

x-client-version 标头重复或包含无效的语义化版本

InvalidGrant

授权凭证类型无效。 请查阅授权指南以获取正确的值。

InvalidRequest

请求参数或负载格式不正确。 请验证字段名称和值格式。

如果在令牌刷新期间收到此错误,则表示您的刷新令牌已过期,您必须重新启动授权流程。

SlowDown

您已超过请求速率限制。 请对后续请求实施指数退避。 请注意,在同一个 TCP 连接上发出多个设备代码请求可能会触发此错误——请针对每个配对请求创建新的连接。

UnauthorizedClient

通常表示 access_token 已过期或缺失。 如果收到此错误,请在重试之前刷新您的令牌。

如果在令牌刷新期间遇到此错误,您必须重新启动授权流程并提示用户重新连接。

当访问设备授权范围之外的资源时,或当某个项目已禁用 C2C 设备时,也可能出现此错误。 请确认您在授权期间已请求了适当的权限范围。

如果此错误发生在令牌刷新期间,则必须在用户干预下重新启动整个授权流程。

后续步骤

我们鼓励您就任何疑问联系我们的团队,并继续参阅高级上传指南。 我们期待为您的集成进度提供支持。 如果您还没有阅读,请在继续之前先查阅实施 C2C:设置指南。 您需要用到在身份验证和授权流程中获取的 access_token。 本指南基于基础上传指南高级上传指南