문서로 건너뛰기
PHP

HTTP 클라이언트

기본은 cURL이며, 직접 고른 클라이언트를 쓰고 싶다면 어떤 PSR-18 클라이언트든, 테스트에는 가짜 클라이언트를 씁니다.

기본값

httpClient:가 없으면 요청은 curl 확장만 있으면 되는 OpenEmail\Http\CurlHttpClient를 거칩니다. 클라이언트마다 하나의 cURL 핸들을 유지하므로 두 번째 요청은 첫 번째 요청이 연 연결을 재사용하며, 프록시, 인증 기관, 추가 cURL 설정에 대한 옵션은 구성 페이지에 나와 있습니다.

바이트를 무엇이 옮기든, 나머지는 모두 클라이언트 자체가 처리합니다: 자격 증명, 멱등성 키, 재시도와 그 백오프, 시도마다의 타임아웃, 실패마다의 예외. HTTP 클라이언트는 요청 하나를 보내고 응답을 돌려줄 뿐입니다.

모든 PSR-18 클라이언트

OpenEmail\Http\Psr18HttpClient는 Guzzle이나 Symfony HttpClient처럼 PSR-18을 구현하는 어떤 클라이언트든 감싸므로, 요청은 애플리케이션이 이미 구성해 둔 클라이언트를 그 미들웨어, 로깅, 프록시 설정과 함께 거칩니다.

guzzle.php
use GuzzleHttp\Client;use OpenEmail\Http\Psr18HttpClient;use OpenEmail\OpenEmail; $client = new OpenEmail(httpClient: new Psr18HttpClient(new Client())); $client->me->ping();
symfony_http_client.php
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로 설치하세요.

factories.php
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를 포함해 모든 상태에 대해 응답을 반환하고, 응답이 전혀 오지 않았을 때만 예외를 던지세요.

클래스담는 것
HttpRequestmethod, url, 이름과 값의 배열인 headers, 문자열 또는 null인 body, 그리고 초 단위이거나 없으면 null인 timeout. header($name)은 대소문자에 상관없이 헤더 하나를 읽습니다. var_dump(), print_r(), json_encode()는 Authorization 헤더를 [redacted]로 표시하지만, var_export()와 Symfony의 dump()는 headers를 그대로 출력하므로 이 두 함수로는 요청을 절대 로그에 남기지 마세요.
HttpResponsenew HttpResponse($status, $headers, $body)로 만듭니다. 헤더 이름을 소문자로 바꾸며, header($name)은 대소문자에 상관없이 헤더 하나를 읽습니다.
HttpClientException응답이 전혀 오지 않았을 때 send에서 던지세요. new HttpClientException($message, timeout: true)는 타임아웃을 표시합니다.

네트워크 없이 테스트하기

가짜 클라이언트는 나갔을 내용을 기록하고 테스트에 필요한 응답을 돌려주므로, 아무것도 컴퓨터 밖으로 나가지 않고 테스트는 무엇이 전송되었는지 정확히 확인할 수 있습니다.

fake_http_client.php
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으로 테스트 클라이언트를 만드세요. 그렇지 않으면 반복해도 안전한 호출에서 재시도 가능한 상태나 네트워크 장애가 나면, 사이사이에 실제로 대기하면서 세 번 시도됩니다.