エラー
すべての失敗は例外をスローします。拒否用のクラスと応答なし用のクラスがあり、すべての API エラーにリクエスト id が付きます。
エラーを捕捉する
use OpenEmail\Exception\ApiException;use OpenEmail\Exception\NetworkException; $message = [ 'from' => '[email protected]', 'to' => '[email protected]', 'subject' => 'Your September invoice', 'text' => 'Invoice attached.',]; try { $client->emails->send($message);} catch (ApiException $error) { if ($error->isValidation()) { error_log($error->errorCode . ' ' . $error->param . ' ' . $error->getMessage()); } if ($error->isPermission()) { $book = $client->addresses->listAll(); error_log('this key may send as ' . implode(', ', array_column($book->addresses, 'address'))); } if ($error->isRateLimited()) { error_log('try again in ' . $error->retryAfterSeconds . ' seconds'); } error_log($error->status . ' ' . $error->requestId); throw $error;} catch (NetworkException $error) { if ($error->isTimeout()) { error_log('no answer in time'); } throw $error;}送信時の permission_error は、たいていワークスペースではなくキーの送信範囲、つまりキーに与えられていないドメインやアドレスが原因です。このサンプルが、このキーで送信できるアドレスとして addresses->listAll() が返すものをログに記録しているのはそのためです。
拒否の種類ごとに専用のサブクラスがあるため、catch は処理するものをクラスで選び、残りは上位へ伝播させることができます。
use OpenEmail\Exception\AuthenticationException;use OpenEmail\Exception\OpenEmailException;use OpenEmail\Exception\PermissionException;use OpenEmail\Exception\ValidationException; $message = [ 'from' => '[email protected]', 'to' => '[email protected]', 'subject' => 'Your September invoice', 'text' => 'Invoice attached.',]; try { $client->emails->send($message);} catch (ValidationException $error) { error_log($error->param . ': ' . $error->getMessage());} catch (AuthenticationException|PermissionException $error) { error_log('the key cannot do this: ' . $error->errorCode); throw $error;} catch (OpenEmailException $error) { error_log($error::class . ': ' . $error->getMessage()); throw $error;}エラークラス
すべてのクラスは OpenEmail\Exception にあります。
| クラス | 発生条件 |
|---|---|
| OpenEmailException | パッケージがスローするすべての例外が実装するインターフェース。そのため catch (OpenEmailException $error) は、InvalidArgumentException も含めてそれらすべてを捕捉します。 |
| ApiException | API が応答したものの、成功ではありませんでした。status、type、errorCode、param、docUrl、requestId、retryAfterSeconds、fields、body を持ちます。サーバー障害のように type が api_error のときはこのクラス自身として、それ以外のときはその type に対応するサブクラスとしてスローされます。RuntimeException を継承しています。 |
| InvalidRequestException、AuthenticationException、PermissionException、NotFoundException、ConflictException、ValidationException、RateLimitException | ApiException のサブクラスで、type ごとに 1 つずつあります。対応する値は invalid_request_error、authentication_error、permission_error、not_found_error、conflict_error、validation_error、rate_limit_error です。 |
| NetworkException | 応答が届きませんでした。原因は DNS、TLS、拒否または切断された接続、あるいはタイムアウトです。getPrevious() が元になった例外を保持し、タイムアウトが原因のときは isTimeout() が true になります。RuntimeException を継承しています。 |
| WebhookSignatureException | OpenEmail::verifyWebhookSignature() が配信を拒否しました。UnexpectedValueException を継承しています。 |
| InvalidArgumentException | 何かが送信される前にスローされます。キーがない、または形式が正しくない場合、baseUrl: が使えない場合、id が空の場合などです。呼び出しそのものが誤っていることを意味するため、PHP 標準の InvalidArgumentException を継承しています。 |
ApiException が持つもの
getMessage()string- API 自身が人向けに書いた文で、問題の値があればそれを示します。安定した識別子ではないため、分岐には `errorCode` を使ってください。
statusint or null- 応答の HTTP ステータスで、`getCode()` も同じ値を返します。成功レスポンスがクライアントの読めない形で返ってきたときだけ null になります。
typestring- `OpenEmail\Constants\ErrorTypes` にある 8 つの値のいずれか。この集合は固定されており、増えることはありません。ボディに指定がなければ、ステータスから推定されます。
errorCodestring- `from_address_forbidden` や `invalid_email_address` のような具体的な失敗。PHP では `code` が `getCode()` の返す数値のために使われているため、`errorCode` という名前になっています。値は追加される可能性があり閉じていないため、知らない値はその `type` として扱ってください。ボディが API のエラーエンベロープではなかった場合は `unrecognised_response` になります。
paramstring or null- 失敗がフィールドを示す場合の、拒否されたフィールド。`to.0` のようなドット区切りのパスです。
docUrlstring or null- API が示す場合の、この失敗に関するページ。
requestIdstring or null- サーバーがリクエストを記録した id。ボディまたは `x-request-id` ヘッダーから取得します。
retryAfterSecondsint, float or null- サーバーが `Retry-After` で求めた待機時間の秒数。数値と日付のどちらで送られても秒数になります。送られなかった場合は null。
fieldsarray or null- 問題ごとに 1 つの配列で、それぞれ `key` と `error` を持ちます。たとえば `forms->subscribe` が 422 `invalid_form_submission` で回答を拒否したときは `['key' => 'email', 'error' => 'email']` のようになります。エラーに一覧がなければ null。
bodymixed- デコードしたエラーレスポンス全体。空だった場合や JSON ではなかった場合は null。
| メソッド | true になる条件 |
|---|---|
| isAuth() | type が authentication_error、すなわち 401 の場合。キーがない、資格情報の種類が違う、あるいは当方が発行していないキーであることを意味する。 |
| isPermission() | permission_error、すなわち 403 の場合。キー自体は有効だが、必要なスコープまたは From アドレスを持っていない。 |
| isScopeMissing() | errorCode が 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 のいずれかの場合。 |
| isStepUpRequired() | errorCode が step_up_required、つまり本人がコードを確認するまで、重要な変更の前に OAuth アクセストークンが受け取る 403 です。 |
これらの多くは、エンベロープのうち固定された側である type を読み、各サブクラスは 1 つの type に対応します。errorCode は文字列のままです。API はこの値が閉じておらず追加されうることを保証しているため、知らない値はその type として扱ってください。閉じた一覧にすると、新しい失敗の種類を読むたびにパッケージの更新が必要になってしまいます。
API のエラーエンベロープではないボディでも ApiException をスローし、type はステータスから推定され、errorCode は unrecognised_response になります。ボディが JSON ではない成功レスポンスも、同様にスローします。
isRetryable() が表すのはステータスであり、あなたの呼び出しではありません。繰り返しても安全な呼び出しは、例外をスローする時点ですでにリトライ済みです。また send_quota_exceeded や ai_quota_exceeded のような 429 は、上限がリセットされるまで同じように失敗するため、ループで再試行せずに人に知らせてください。
HTTP クライアントがスローした例外は、その呼び出しに許されたリトライを使い切ると NetworkException になり、元の例外は getPrevious() で取得できます。LogicException と、TypeError のようなあらゆる Error は HTTP クライアントのバグを意味するため、そのままスローされ、リトライされることはありません。Psr18HttpClient は、ラップしたクライアントの例外からメッセージだけを残します。その例外がリクエストとその Authorization ヘッダーを保持しているからです。
requestId
すべての ApiException は、サーバーが送ったリクエスト id をエラーボディまたは x-request-id ヘッダーから受け取って保持しており、これがあなたの失敗をサーバーのログの 1 行と結びつける唯一の手がかりです。成功時はデコード済みのボディだけを返すため、成功レスポンスで読めるリクエスト id はありません。