*/ public function backoff(): array { return [60, 300, 1800, 7200]; } public function handle(PublicEndpoint $endpoint): void { $delivery = $this->delivery; $webhook = $delivery->webhook; if ($webhook === null || ! $webhook->is_active) { return; } $delivery->increment('attempts'); $body = (string) json_encode([ 'id' => $delivery->uuid, 'event' => $delivery->event, 'created_at' => $delivery->created_at?->toIso8601String(), 'data' => $delivery->payload, ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); [$address, $refused] = $endpoint->check($webhook->url); // Checked again at every attempt, not only when the URL was saved: a // name can be pointed somewhere else in between. if ($refused !== null) { $this->failed(new RuntimeException(__("rules.media.remote.{$refused}"))); $this->delete(); return; } $parts = parse_url($webhook->url); $timestamp = (string) now()->timestamp; try { $response = Http::withOptions([ 'allow_redirects' => false, 'connect_timeout' => 5, 'timeout' => 10, 'curl' => [CURLOPT_RESOLVE => [sprintf( '%s:%d:%s', trim((string) $parts['host'], '[]'), (int) ($parts['port'] ?? 443), str_contains((string) $address, ':') ? "[{$address}]" : $address, )]], ]) ->withHeaders([ 'Content-Type' => 'application/json', 'X-Webhook-Event' => $delivery->event, 'X-Webhook-Delivery' => $delivery->uuid, 'X-Webhook-Timestamp' => $timestamp, 'X-Webhook-Signature' => 'sha256='.hash_hmac('sha256', "{$timestamp}.{$body}", (string) $webhook->secret), ]) ->withUserAgent(sprintf('%s/1.0 (+%s; webhooks)', Str::slug((string) config('app.name')) ?: 'landing', rtrim((string) config('app.url'), '/'))) ->withBody($body, 'application/json') ->post($webhook->url); } catch (ConnectionException $e) { $this->retryOrFail($e->getMessage(), null); return; } if ($response->successful()) { $delivery->update([ 'status' => WebhookDelivery::SUCCEEDED, 'response_status' => $response->status(), 'error' => null, 'delivered_at' => now(), ]); $webhook->forceFill(['consecutive_failures' => 0, 'last_delivered_at' => now()])->save(); return; } $this->retryOrFail(Str::limit($response->body(), 500), $response->status()); } public function failed(?\Throwable $e): void { $this->delivery->update([ 'status' => WebhookDelivery::FAILED, 'error' => Str::limit((string) $e?->getMessage(), 500), ]); $webhook = $this->delivery->webhook; if ($webhook !== null) { $failures = $webhook->consecutive_failures + 1; $webhook->forceFill([ 'consecutive_failures' => $failures, 'is_active' => $failures < Webhook::MAX_FAILURES && $webhook->is_active, ])->save(); } } protected function retryOrFail(string $error, ?int $status): void { $this->delivery->update(['response_status' => $status, 'error' => $error]); if ($this->attempts() < $this->tries) { $this->release($this->backoff()[$this->attempts() - 1] ?? 7200); return; } $this->failed(new RuntimeException($error)); $this->delete(); } }