Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 51 additions & 32 deletions src/CallWebhookJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
namespace Spatie\WebhookServer;

use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Response as GuzzleResponse;
use GuzzleHttp\TransferStats;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Client\Response as HttpClientResponse;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Spatie\WebhookServer\BackoffStrategy\BackoffStrategy;
use Spatie\WebhookServer\Events\FinalWebhookCallFailedEvent;
use Spatie\WebhookServer\Events\WebhookCallFailedEvent;
use Spatie\WebhookServer\Events\WebhookCallSucceededEvent;
Expand Down Expand Up @@ -47,6 +49,8 @@ class CallWebhookJob implements ShouldQueue

public array $headers = [];

public array $options = [];

public string|bool $verifySsl;

public bool $throwExceptionOnFailure;
Expand All @@ -72,7 +76,7 @@ class CallWebhookJob implements ShouldQueue

protected ?TransferStats $transferStats = null;

public function handle()
public function handle(): void
{
$lastAttempt = $this->attempts() >= $this->tries;

Expand All @@ -91,19 +95,15 @@ public function handle()

return;
} catch (Exception $exception) {
if ($exception instanceof RequestException) {
$this->response = $exception->getResponse();
$this->errorType = get_class($exception);
$this->errorMessage = $exception->getMessage();
}
$this->errorType = get_class($exception);
$this->errorMessage = $exception->getMessage();

if ($exception instanceof ConnectException) {
$this->errorType = get_class($exception);
$this->errorMessage = $exception->getMessage();
if ($exception instanceof RequestException) {
$this->response = $this->toGuzzleResponse($exception->response);
}

if (! $lastAttempt) {
/** @var \Spatie\WebhookServer\BackoffStrategy\BackoffStrategy $backoffStrategy */
/** @var BackoffStrategy $backoffStrategy */
$backoffStrategy = app($this->backoffStrategyClass);

$waitInSeconds = $backoffStrategy->waitInSecondsAfterAttempt($this->attempts());
Expand All @@ -121,6 +121,19 @@ public function handle()
}
}

protected function toGuzzleResponse(HttpClientResponse $response): GuzzleResponse
{
$psrResponse = $response->toPsrResponse();

return new GuzzleResponse(
$psrResponse->getStatusCode(),
$psrResponse->getHeaders(),
$psrResponse->getBody(),
$psrResponse->getProtocolVersion(),
$psrResponse->getReasonPhrase()
);
}

public function tags(): array
{
return $this->tags;
Expand All @@ -131,29 +144,35 @@ public function getResponse(): ?Response
return $this->response;
}

protected function getClient(): ClientInterface
{
return app(Client::class);
}

protected function createRequest(array $body): Response
{
$client = $this->getClient();

return $client->request($this->httpVerb, $this->webhookUrl, array_merge(
[
'timeout' => $this->requestTimeout,
'verify' => $this->verifySsl,
'headers' => $this->headers,
'on_stats' => function (TransferStats $stats) {
$this->transferStats = $stats;
},
],
$body,
$request = Http::withHeaders($this->headers)
->timeout($this->requestTimeout)
->unless($this->outputType === 'JSON', function (PendingRequest $request) {
$request->withHeaders([
'Content-Type' => "text/xml;charset=utf-8"
]);
})
->unless($this->verifySsl, fn(PendingRequest $request) => $request->withoutVerifying());

$request->withOptions(array_merge($this->options, [
is_null($this->proxy) ? [] : ['proxy' => $this->proxy],
is_null($this->cert) ? [] : ['cert' => [$this->cert, $this->certPassphrase]],
is_null($this->sslKey) ? [] : ['ssl_key' => [$this->sslKey, $this->sslKeyPassphrase]]
));
]));

$response = match (strtoupper($this->httpVerb)) {
'GET' => $request->get($this->webhookUrl, $body['query']),
'POST' => $request->post($this->webhookUrl, $body),
'PUT' => $request->put($this->webhookUrl, $body),
'PATCH' => $request->patch($this->webhookUrl, $body),
};


$this->transferStats = $response->transferStats;
$response->throw();

return $this->toGuzzleResponse($response);
}

protected function shouldBeRemovedFromQueue(): bool
Expand Down
61 changes: 47 additions & 14 deletions tests/CallWebhookJobTest.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<?php

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\TransferStats;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;
use Mockery\MockInterface;
use function Pest\Laravel\artisan;
use function Pest\Laravel\mock;
Expand Down Expand Up @@ -70,13 +69,11 @@ function baseGetRequest(array $overrides = []): array
Event::fake();

$this->testClient = new TestClient();

app()->bind(Client::class, function () {
return $this->testClient;
});
});

it('can make a webhook call', function () {
Http::fake();

baseWebhook()->dispatch();

artisan('queue:work --once');
Expand All @@ -87,6 +84,8 @@ function baseGetRequest(array $overrides = []): array
});

it('can make a legacy synchronous webhook call', function () {
Http::fake();

baseWebhook()->dispatchSync();

$this
Expand All @@ -95,6 +94,8 @@ function baseGetRequest(array $overrides = []): array
});

it('can make a synchronous webhook call', function () {
Http::fake();

baseWebhook()->dispatchSync();

$this
Expand All @@ -103,6 +104,8 @@ function baseGetRequest(array $overrides = []): array
});

it('can use a different HTTP verb', function () {
Http::fake();

baseWebhook()
->useHttpVerb('put')
->dispatch();
Expand All @@ -117,6 +120,8 @@ function baseGetRequest(array $overrides = []): array
});

it('uses query option when http verb is get', function () {
Http::fake();

baseWebhook()
->useHttpVerb('get')
->dispatch();
Expand All @@ -131,6 +136,8 @@ function baseGetRequest(array $overrides = []): array
});

it('can add extra headers', function () {
Http::fake();

$extraHeaders = [
'Content-Type' => 'application/json',
'header1' => 'value1',
Expand All @@ -156,6 +163,8 @@ function baseGetRequest(array $overrides = []): array
});

it('will not set a signature header when the request should not be signed', function () {
Http::fake();

baseWebhook()
->doNotSign()
->dispatch();
Expand All @@ -172,6 +181,8 @@ function baseGetRequest(array $overrides = []): array
});

it('can disable verifying SSL', function () {
Http::fake();

baseWebhook()->doNotVerifySsl()->dispatch();

$baseRequest = baseRequest();
Expand All @@ -185,6 +196,8 @@ function baseGetRequest(array $overrides = []): array
});

it('will use mutual TLS without passphrases', function () {
Http::fake();

baseWebhook()
->mutualTls('foobar', 'barfoo')
->dispatch();
Expand All @@ -202,6 +215,8 @@ function baseGetRequest(array $overrides = []): array
});

it('will use mutual TLS with passphrases', function () {
Http::fake();

baseWebhook()
->mutualTls('foobar', 'barfoo', 'foobarpassword', 'barfoopassword')
->dispatch();
Expand All @@ -219,6 +234,8 @@ function baseGetRequest(array $overrides = []): array
});

it('will use mutual TLS with certificate authority', function () {
Http::fake();

baseWebhook()
->mutualTls('foobar', 'barfoo')
->verifySsl('foofoo')
Expand All @@ -238,6 +255,8 @@ function baseGetRequest(array $overrides = []): array
});

it('will use a proxy', function () {
Http::fake();

baseWebhook()
->useProxy('https://proxy.test')
->dispatch();
Expand All @@ -254,6 +273,8 @@ function baseGetRequest(array $overrides = []): array
});

it('will use a proxy array', function () {
Http::fake();

baseWebhook()
->useProxy([
'http' => 'http://proxy.test',
Expand All @@ -276,14 +297,16 @@ function baseGetRequest(array $overrides = []): array
});

test('by default it will retry 3 times with the exponential backoff strategy', function () {
$this->testClient->letEveryRequestFail();
Http::fake([
'*' => Http::response(status: 500)
]);

baseWebhook()->dispatch();

mock(ExponentialBackoffStrategy::class, function (MockInterface $mock) {
$mock->shouldReceive('waitInSecondsAfterAttempt')->withArgs([1])->once()->andReturns(10);
$mock->shouldReceive('waitInSecondsAfterAttempt')->withArgs([2])->once()->andReturns(100);
$mock->shouldReceive('waitInSecondsAfterAttempt')->withArgs([3])->never();
$mock->expects('waitInSecondsAfterAttempt')->withArgs([1])->andReturns(10);
$mock->expects('waitInSecondsAfterAttempt')->withArgs([2])->andReturns(100);
$mock->allows('waitInSecondsAfterAttempt')->withArgs([3])->never();

return $mock;
});
Expand Down Expand Up @@ -313,7 +336,9 @@ function baseGetRequest(array $overrides = []): array
});

it('sets the response field on request failure', function () {
$this->testClient->throwRequestException();
Http::fake([
'*' => Http::response(status: 500)
]);

baseWebhook()->dispatch();

Expand All @@ -326,7 +351,9 @@ function baseGetRequest(array $overrides = []): array
});

it('sets the error fields on connection failure', function () {
$this->testClient->throwConnectionException();
Http::fake(
['*' => Http::response(status: 500)]
);

baseWebhook()->dispatch();

Expand All @@ -341,20 +368,26 @@ function baseGetRequest(array $overrides = []): array
});

it('generate job failed event if an exception throws and throw exception on failure config is set', function () {
$this->testClient->throwConnectionException();
Http::fake(
[
'*' => Http::response(status: 500)
]
);

baseWebhook()->maximumTries(1)->throwExceptionOnFailure()->dispatch();

artisan('queue:work --once');

Event::assertDispatched(JobFailed::class, function (JobFailed $event) {
expect($event->exception)->toBeInstanceOf(ConnectException::class);
expect($event->exception)->toBeInstanceOf(Illuminate\Http\Client\RequestException::class);

return true;
});
});

it('send raw body data if rawBody is set', function () {
Http::fake();

$testBody = "<xml>anotherOption</xml>";
WebhookCall::create()
->url('https://example.com/webhooks')
Expand Down
Loading