오류
모든 실패는 예외를 던집니다. 거부에는 하나의 클래스, 응답 없음에는 또 하나의 클래스가 있으며, 모든 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 | type마다 하나씩 있는 ApiException의 하위 클래스입니다: 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`의 여덟 값 중 하나로, 이 집합은 고정되어 늘어나지 않습니다. 본문에 없으면 상태 코드에서 추론합니다.
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- 문제마다 하나의 배열이며, 각각 `key`와 `error`를 가집니다. 예를 들어 `forms->subscribe`가 422 `invalid_form_submission`으로 응답을 거부했다면 `['key' => 'email', 'error' => 'email']`과 같습니다. 오류가 아무것도 나열하지 않으면 null입니다.
bodymixed- 디코딩된 오류 응답 전체입니다. 비어 있거나 JSON이 아니었다면 null입니다.
| 메서드 | true가 되는 조건 |
|---|---|
| isAuth() | type이 authentication_error인 401입니다. 키가 없거나, 자격 증명의 종류가 잘못되었거나, OpenEmail이 발급하지 않은 키입니다. |
| 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을 읽으며, 각 하위 클래스는 하나의 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 헤더에서 가져와 담고 있으며, 이 값만이 여러분의 실패를 서버 로그의 한 줄과 이어 줍니다. 성공하면 디코딩된 본문만 반환되므로 거기서 읽을 요청 id는 없습니다.