Практическое руководство: обработка ошибок

Введение

В этом руководстве рассматривается обработка ошибок при взаимодействии с API-интерфейсом C2C. Корректная обработка ошибок HTTP — важнейшая часть надежной интеграции с любой сторонней службой.

Типы ошибок

Ошибки в вашей интеграции могут появляться из различных источников, которые можно разделить на четыре основные группы:

  • Ошибки ввода-вывода: возникают в результате аппаратных операций на вашем устройстве, например при сбоях операций чтения/записи.
  • Ошибки приложения: возникают из-за проблем в коде вашего приложения.
  • Сетевые ошибки: возникают в сетевом стеке и передаются вашей сетевой библиотекой.
  • Ошибки API-интерфейса: генерируются серверными службами Frame.io.

Для каждой категории ошибок нужен свой подход к обработке. Это руководство посвящено в основном ошибкам API-интерфейса, но мы рассмотрим и общие стратегии для других категорий.

Как API-интерфейс возвращает ошибки

API-интерфейс Frame.io передает ошибки с помощью двух основных механизмов:

  • Коды состояния: коды ошибок HTTP, которые указывают на характер проблемы.
  • Сообщения об ошибках: содержимое нагрузки, которое предоставляет дополнительные сведения об ошибке, особенно когда несколько разных ошибок имеют одинаковый код состояния.

Коды состояния ошибок

Коды состояния HTTP — это стандартизированные числовые ответы, которые сообщают результат HTTP-запроса. Подробнее см. документацию по кодам состояния HTTP от Mozilla или HTTP Cats с очень наглядными иллюстрациями. Каждая конечная точка API-интерфейса Frame.io указывает ожидаемый код состояния успеха — обычно 200 (OK), 201 (Created) или 204 (No Content). Вы можете убедиться, что запрос успешно выполнен, либо проверив эти конкретные коды, либо убедившись, что код находится в диапазоне 200–299. Коды состояния выше 399 указывают на ошибки. Большинство ошибок API-интерфейса возвращают коды 4XX (400-499), указывая на проблемы на стороне клиента. Ошибки за пределами этого диапазона обычно возникают в сетевой инфраструктуре между вашим устройством и нашей службой, за исключением ошибки 500 (Internal Server Error), которая указывает на неожиданную проблему на нашем сервере. Кроме того, ответ 404 (Not Found) может быть передан промежуточными сервисами, а не нашим сервером, несмотря на то что это код 4XX.

Если вы получили неожиданный код состояния, сообщите об этом нашей команде.

Схемы данных об ошибках

Frame.io возвращает сведения об ошибках в двух форматах: простом и подробном. Ваша логика обработки ошибок должна учитывать оба формата.

Простая схема информации об ошибках

Вот пример запроса с неправильным 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}

Подробные ошибки содержат поле message, которое описывает тип ошибки.

Определение типа ошибки

При обработке ошибок 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. Как правило, при несущественных ошибках 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-интерфейса нужно сделать еще одну попытку. Что касается ошибок, вызванных не API-интерфейсом, а операциями ввода-вывода, сетевыми библиотеками или AWS, рекомендуем повторять те, которые могут возникнуть из-за временных условий. Перегрузка сети, временные отключения сервера или потеря пакетов обычно требуют повторных попыток. Большинство сетевых библиотек вызывают ошибку 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).

Экспоненциальная задержка необходима для ошибок, вызванных ограничением скорости, а также полезна для обработки сетевых и ошибок ввода-вывода в целом. Такой подход позволяет дождаться, пока временные ограничения ресурсов разрешатся, не создавая дополнительную нагрузку повторными запросами.

Обнаружение состояния отключения

Сетевые ошибки могут указывать на то, что 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

Таблицы ошибок

В следующих таблицах приведены категории ошибок API-интерфейса Frame.io и рекомендации по их обработке. Вот что означает каждый столбец:

Сообщение: идентификатор сообщения структурированных данных об ошибке Код HTTP: код состояния HTTP Тип ошибки: логическая категория ошибки (подробно описана в разделе Описания) Схема: формат структурированных данных об ошибке (простой или подробный) Повтор: рекомендация по повторным попыткам (да — несколько попыток, одна — одна попытка, нет — критическая ошибка) Звездочки (*) означают, что в разделе Описания приведены особые указания.

Сообщения об ошибках Frame.io

СообщениеТип ошибкиКод HTTPСхемаПовтор
”access_denied”AccessDenied401простойодна
”authorization_pending”AuthorizationPending400простойда
”Channel Paused”ChannelPaused409простойнет
”expired_token”ExpiredToken400простойнет
”Invalid Argument”InvalidArgument422подробныйнет
”invalid_client”InvalidClient400простойнет
”Invalid client version”InvalidClientVersion400простойнет
”invalid_grant”InvalidGrant400простойнет
”invalid_request”InvalidRequest400простойодна
”Not Authorized”UnauthorizedClient401подробныйнет
”slow_down”SlowDown400простойда
”unauthorized_client”UnauthorizedClient401простойда*

Коды состояния Frame.io

Код HTTPТип ошибкиПовтор
400InvalidRequestодна
401UnauthorizedClientнет
422InvalidContentTypeнет
429SlowDownда
500InternalServerErrorда

Ошибки AWS

Подробное описание см. в документации AWS.

ОшибкаПовтор
InternalErrorда
OperationAbortedда
RequestTimeoutда
ServiceUnavailableда
SlowDownда
[Все остальные ошибки]одна
Разбор похожих ошибок AWS

Как SlowDown, так и ServiceUnavailable, возвращенные AWS, указывают на проблемы с частотой запросов и могут обрабатываться аналогично ошибке SlowDown, возвращенной Frame.io, с использованием экспоненциальной задержки. Аналогичным образом ошибка InternalError, возвращенная AWS, по смыслу соответствует ошибке InternalServerError в нашем API-интерфейсе.

Описания

AccessDenied

Возвращается, когда пользователь отклоняет авторизацию во время сопряжения устройства.

AuthorizationPending

Указывает, что пользователь еще не ввел код сопряжения устройства. Продолжайте опрос после периода interval, указанного в ответе с кодом устройства.

ChannelPaused

Канал устройства был приостановлен при создании ресурса. Не пытайтесь добавить этот ресурс снова.

ExpiredToken

Срок действия кода сопряжения устройства истек. Создайте новый код и перезапустите процесс сопряжения.

InternalServerError

Указывает на неожиданную проблему сервера. Повторите попытку один раз. Сообщайте нашей команде об ошибках 500, чтобы мы провели расследование. Обратите внимание: что некоторые известные проблемы возвращают ошибку 500, хотя должны возвращать InvalidRequest:

  • Попытка добавления в несуществующий канал устройства
  • Запрос недопустимого количества пользовательских фрагментов

InvalidArgument

Параметр полезной нагрузки содержал недопустимое значение. Убедитесь, что значения параметров соответствуют ожиданиям API-интерфейса.

InvalidContentType

Заголовок Content-Type запроса не поддерживается. API-интерфейс обычно принимает:

  • form/multipart (только конечные точки авторизации)
  • application/x-www-form-urlencoded (все конечные точки)
  • application/json (конечные точки без авторизации)

InvalidClient

Предоставленные учетные данные (client_id, client_secret и т. д.) не распознаны. Проверьте учетные данные интеграции.

InvalidClientVersion

Заголовок x-client-version был продублирован или содержит недопустимую семантическую версию.

InvalidGrant

Тип разрешения авторизации недействителен. Правильные значения приведены в руководствах по авторизации.

InvalidRequest

Параметры запроса или формат полезной нагрузки неверны. Проверьте имена полей и форматы значений.

Если ошибка возвращена во время обновления токена, ваш срок действия токена обновления истек, и нужно перезапустить процесс авторизации.

SlowDown

Вы превысили ограничение частоты запросов. Используйте экспоненциальную задержку для последующих запросов. Обратите внимание, что эту ошибку может вызвать выполнение нескольких запросов кода устройства через одно и то же подключение TCP — создавайте новые подключения для каждого запроса сопряжения.

UnauthorizedClient

Обычно указывает на истекший или отсутствующий access_token. Если возвращена эта ошибка, обновите токен перед повторной попыткой.

Если ошибка возникает при обновлении токена, необходимо перезапустить процесс авторизации и предложить пользователю повторно подключиться.

Эта ошибка также может возникнуть при доступе к ресурсам вне области авторизации вашего устройства или когда проект отключил устройства C2C. Убедитесь, что вы запросили соответствующие области во время авторизации.

Если эта ошибка возникает при обновлении токена, весь процесс авторизации должен быть перезапущен с участием пользователя.

Дальнейшие шаги

Мы рекомендуем обращаться к нашей команде с любыми вопросами и переходить к руководству по расширенному добавлению. Мы с удовольствием поможем вам в продолжении изучения процессов интеграций. Если вы еще не сделали этого, просмотрите руководство Реализация C2C: настройка перед продолжением. Вам понадобится access_token, полученный в процессе аутентификации и авторизации. Это руководство основано на руководстве по базовому добавлению и руководстве по расширенному добавлению.