HTTP 클라이언트
기본은 cURL이며, 직접 고른 클라이언트를 쓰고 싶다면 어떤 PSR-18 클라이언트든, 테스트에는 가짜 클라이언트를 씁니다.
기본값
httpClient:가 없으면 요청은 curl 확장만 있으면 되는 OpenEmail\Http\CurlHttpClient를 거칩니다. 클라이언트마다 하나의 cURL 핸들을 유지하므로 두 번째 요청은 첫 번째 요청이 연 연결을 재사용하며, 프록시, 인증 기관, 추가 cURL 설정에 대한 옵션은 구성 페이지에 나와 있습니다.
바이트를 무엇이 옮기든, 나머지는 모두 클라이언트 자체가 처리합니다: 자격 증명, 멱등성 키, 재시도와 그 백오프, 시도마다의 타임아웃, 실패마다의 예외. HTTP 클라이언트는 요청 하나를 보내고 응답을 돌려줄 뿐입니다.
모든 PSR-18 클라이언트
OpenEmail\Http\Psr18HttpClient는 Guzzle이나 Symfony HttpClient처럼 PSR-18을 구현하는 어떤 클라이언트든 감싸므로, 요청은 애플리케이션이 이미 구성해 둔 클라이언트를 그 미들웨어, 로깅, 프록시 설정과 함께 거칩니다.
use GuzzleHttp\Client;use OpenEmail\Http\Psr18HttpClient;use OpenEmail\OpenEmail; $client = new OpenEmail(httpClient: new Psr18HttpClient(new Client())); $client->me->ping();use OpenEmail\Http\Psr18HttpClient;use OpenEmail\OpenEmail;use Symfony\Component\HttpClient\HttpClient;use Symfony\Component\HttpClient\Psr18Client; $http = new Psr18Client(HttpClient::create(['timeout' => 30, 'max_redirects' => 0])); $client = new OpenEmail(httpClient: new Psr18HttpClient($http)); $client->me->ping();PSR-18은 PSR-17 팩토리로 요청을 만듭니다. 어댑터는 Symfony의 Psr18Client처럼 클라이언트 자체가 팩토리이기도 하면 그것을 쓰고, 그다음 설치되어 있다면 php-http/discovery를, 그다음 nyholm/psr7이나 Guzzle 자체의 팩토리를 쓰며, 아무것도 찾지 못하면 무엇을 설치해야 하는지 알려 주는 InvalidArgumentException을 던집니다. 직접 고르려면 requestFactory:와 streamFactory:를 전달하세요. Symfony의 Psr18Client에는 PSR-18 인터페이스와 PSR-17 구현체도 필요하므로 composer require symfony/http-client psr/http-client nyholm/psr7로 설치하세요.
use GuzzleHttp\Client;use Nyholm\Psr7\Factory\Psr17Factory;use OpenEmail\Http\Psr18HttpClient;use OpenEmail\OpenEmail; $factory = new Psr17Factory(); $client = new OpenEmail(httpClient: new Psr18HttpClient(new Client(), requestFactory: $factory, streamFactory: $factory)); $client->me->ping();| 항목 | 일어나는 일 |
|---|---|
| 타임아웃 | Guzzle은 모든 요청에서 클라이언트의 timeout:을 받습니다. PSR-18에는 다른 클라이언트에 타임아웃을 전달할 방법이 없으므로, 위의 HttpClient::create(['timeout' => 30, 'max_redirects' => 0])처럼 감싸는 클라이언트에 타임아웃을 설정하세요. 그렇지 않으면 응답하지 않는 요청이 영원히 기다릴 수 있습니다. |
| 오류 상태 | PSR-18 클라이언트는 4xx와 5xx 응답에 예외를 던지지 않고 응답을 반환하며, 어댑터는 Guzzle에게도 그렇게 하도록 지시하므로, 모든 거부는 여전히 올바른 ApiException이 됩니다. |
| 응답 없음 | 감싼 클라이언트의 ClientExceptionInterface는 그 메시지를 유지한 NetworkException이 되지만, 예외 자체는 보관하지 않습니다. 그 예외가 요청과 그 Authorization 헤더를 담고 있기 때문입니다. 메시지가 요청 시간 초과를 말하면 isTimeout()이 true이며, Guzzle이 아닌 클라이언트에서는 그것이 감싼 클라이언트 자체의 시간 제한이었다고 메시지가 알려 줍니다. |
| 리디렉션 | Guzzle에게는 리디렉션을 따라가지 말라고 지시하고, Symfony 예제는 max_redirects를 0으로 설정합니다. 따라서 기본 cURL 클라이언트와 마찬가지로 리디렉션은 ApiException이 됩니다. |
직접 만든 클라이언트
httpClient:는 OpenEmail\Http\HttpClient를 구현하는 것이면 무엇이든 받습니다. 이 인터페이스에는 메서드가 하나뿐입니다: send(HttpRequest $request): HttpResponse. 4xx와 5xx를 포함해 모든 상태에 대해 응답을 반환하고, 응답이 전혀 오지 않았을 때만 예외를 던지세요.
| 클래스 | 담는 것 |
|---|---|
| HttpRequest | method, url, 이름과 값의 배열인 headers, 문자열 또는 null인 body, 그리고 초 단위이거나 없으면 null인 timeout. header($name)은 대소문자에 상관없이 헤더 하나를 읽습니다. var_dump(), print_r(), json_encode()는 Authorization 헤더를 [redacted]로 표시하지만, var_export()와 Symfony의 dump()는 headers를 그대로 출력하므로 이 두 함수로는 요청을 절대 로그에 남기지 마세요. |
| HttpResponse | new HttpResponse($status, $headers, $body)로 만듭니다. 헤더 이름을 소문자로 바꾸며, header($name)은 대소문자에 상관없이 헤더 하나를 읽습니다. |
| HttpClientException | 응답이 전혀 오지 않았을 때 send에서 던지세요. new HttpClientException($message, timeout: true)는 타임아웃을 표시합니다. |
네트워크 없이 테스트하기
가짜 클라이언트는 나갔을 내용을 기록하고 테스트에 필요한 응답을 돌려주므로, 아무것도 컴퓨터 밖으로 나가지 않고 테스트는 무엇이 전송되었는지 정확히 확인할 수 있습니다.
use OpenEmail\Http\HttpClient;use OpenEmail\Http\HttpRequest;use OpenEmail\Http\HttpResponse;use OpenEmail\OpenEmail; $fake = new class implements HttpClient { public array $requests = []; public function send(HttpRequest $request): HttpResponse { $this->requests[] = $request; return new HttpResponse( 200, ['content-type' => 'application/json'], json_encode(['id' => 'msg_test', 'status' => 'sent', 'replayed' => false], JSON_THROW_ON_ERROR), ); }}; $testClient = new OpenEmail(apiKey: 'oe_test_fake', httpClient: $fake, maxRetries: 0, disableUpdateNotice: true); $sent = $testClient->emails->send([ 'from' => '[email protected]', 'to' => '[email protected]', 'subject' => 'Hi', 'text' => 'Hello',]); $request = $fake->requests[0]; echo $sent['status'], ' ', $request->method, ' ', $request->url, ' ', $request->header('Idempotency-Key'), PHP_EOL;var_dump($request);{"error": {"type": "validation_error", "code": "invalid_parameter", "message": "..."}}처럼 API의 오류 봉투를 본문으로 하여 2xx 이외의 상태를 반환하면, 그에 맞는ApiException하위 클래스를 얻습니다.send에서new HttpClientException('timed out', timeout: true)를 던지면isTimeout()이 true인NetworkException을 얻습니다.RuntimeException같은 그 밖의Exception은isTimeout()이 false인NetworkException이 됩니다.LogicException과TypeError같은 모든Error는 가짜 클라이언트의 버그로 간주됩니다. 그대로 던져지며 재시도되지 않습니다.
실패를 연출하는 테스트에서는 maxRetries: 0으로 테스트 클라이언트를 만드세요. 그렇지 않으면 반복해도 안전한 호출에서 재시도 가능한 상태나 네트워크 장애가 나면, 사이사이에 실제로 대기하면서 세 번 시도됩니다.