Ошибки
Любой сбой выбрасывает исключение. Два класса и идентификатор запроса в каждой ошибке API.
Как его поймать
import { OpenEmailApiError, OpenEmailNetworkError, openemail } from '@openemail/sdk' try { await openemail.emails.send(message)} catch (error) { if (error instanceof OpenEmailApiError) { if (error.isValidation) console.error(error.code, error.param, error.message) if (error.isPermission) console.error(await openemail.addresses.list()) if (error.isRateLimited) console.error('try again in', error.retryAfterSeconds, 'seconds') console.error(error.status, error.requestId) } if (error instanceof OpenEmailNetworkError && error.isTimeout) console.error('no answer in time') throw error}permission_error при отправке — это обычно про область отправки самого КЛЮЧА: домен или адрес, которые ему не выданы, а не про рабочее пространство; поэтому пример печатает то, что addresses.list() сообщает о том, от чьего имени этот ключ может отправлять.
Классы
| Класс | Когда |
|---|---|
| `OpenEmailApiError` | API ответил, и ответ не был успешным. Несёт status, type, code, param, docUrl, requestId и retryAfterSeconds. |
| `OpenEmailNetworkError` | Ответ не пришёл: DNS, TLS, оборванное соединение, таймаут или ваш собственный AbortSignal. Несёт cause, а isTimeout равно true, когда причиной был таймаут. |
| `Error` | Выбрасывается до того, как что-либо отправлено: отсутствующий или некорректный ключ, непригодный baseUrl, браузер, пустой идентификатор. |
| Геттер | True, когда |
|---|---|
| `isAuth` | type равен authentication_error, 401: ключа нет, учётные данные не того типа или ключ, который выдавали не мы. |
| `isPermission` | permission_error, 403: настоящий ключ без нужной области или без нужного адреса From. |
| `isScopeMissing` | code равен insufficient_scope — тот самый 403, который называет недостающую область. |
| `isInvalidRequest` | invalid_request_error, 400: запрос, который не удалось разобрать. Письмо сверх предельного размера возвращается как 422 message_too_large, поэтому ловит его геттер isValidation. |
| `isValidation` | validation_error, 422: схема отклонила запрос, а param называет поле. |
| `isNotFound` | not_found_error, 404: такого ресурса нет. |
| `isConflict` | conflict_error, 409: ресурс уже прошёл ту точку, в которой с ним можно было это сделать. |
| `isRateLimited` | rate_limit_error, 429. retryAfterSeconds содержит время ожидания, если сервер его назвал. |
| `isServerError` | status равен 500 или выше. При обращении в поддержку укажите requestId. |
| `isRetryable` | status равен 408, 429, 500, 502, 503 или 504. |
Геттеры читают type — замороженную половину конверта. code остаётся строкой, потому что API гарантирует, что он открыт и только пополняется; незнакомый код трактуйте по его type. Закрытое объединение сделало бы обновление SDK платой за чтение нового вида отказа.
Тело, которое не является конвертом ошибки API, всё равно становится OpenEmailApiError: type выводится из статуса, а code устанавливается в unrecognised_response. Успешный ответ, тело которого не JSON, тоже приводит к такому исключению.
Прерывание — это тоже OpenEmailNetworkError, произошло оно во время запроса или во время ожидания перед повтором, и само прерывание сохраняется в cause. Проверяйте signal.aborted, когда нужно отличить собственную отмену от сетевого сбоя.
requestId
Каждый OpenEmailApiError несёт идентификатор запроса, присланный сервером, — из тела ошибки или из заголовка x-request-id, и это единственное, что связывает ваш сбой со строкой в логе сервера. Успешный вызов разрешается только разобранным телом, поэтому идентификатора запроса на нём нет.