Implement webhook inspector

This commit is contained in:
2026-08-04 17:09:39 +02:00
parent b021836c14
commit 75d8b25b64
57 changed files with 5110 additions and 230 deletions
+105
View File
@@ -0,0 +1,105 @@
<?php
namespace App\Actions;
use App\Events\WebhookRequestReceived;
use App\Models\WebhookEndpoint;
use App\Models\WebhookRequest;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\HttpException;
class CaptureWebhookRequestAction
{
public function handle(WebhookEndpoint $endpoint, Request $request): WebhookRequest
{
$body = $request->getContent();
$bodySize = strlen($body);
if ($bodySize > (int) config('webhooks.max_body_bytes', 1024 * 1024)) {
throw new HttpException(413, 'The webhook request body is too large.');
}
$webhookRequest = DB::transaction(function () use ($endpoint, $request, $body, $bodySize): WebhookRequest {
$lockedEndpoint = WebhookEndpoint::query()
->whereKey($endpoint->getKey())
->lockForUpdate()
->first();
if (! $lockedEndpoint instanceof WebhookEndpoint || ! $lockedEndpoint->acceptsRequests()) {
throw (new ModelNotFoundException)->setModel(WebhookEndpoint::class, [$endpoint->getKey()]);
}
$webhookRequest = WebhookRequest::create([
'webhook_endpoint_id' => $lockedEndpoint->getKey(),
'method' => Str::upper($request->method()),
'request_uri' => $request->getRequestUri(),
'headers' => $request->headers->all(),
'query_parameters' => $request->query(),
'body' => $body,
'json_payload' => $this->parseJsonPayload($body, $request->header('Content-Type')),
'content_type' => $request->header('Content-Type'),
'body_size' => $bodySize,
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'received_at' => now(),
]);
$lockedEndpoint->forceFill([
'last_request_at' => $webhookRequest->received_at,
])->save();
$this->pruneExcessRequests($lockedEndpoint);
return $webhookRequest;
});
WebhookRequestReceived::dispatch(
(string) $endpoint->getKey(),
(string) $webhookRequest->getKey(),
);
return $webhookRequest;
}
/**
* @return array<string, mixed>|null
*/
private function parseJsonPayload(string $body, ?string $contentType): ?array
{
if ($body === '' || $contentType === null) {
return null;
}
$normalizedContentType = Str::lower($contentType);
if (! Str::contains($normalizedContentType, ['application/json', '+json'])) {
return null;
}
try {
$payload = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException) {
return null;
}
return is_array($payload) ? $payload : null;
}
private function pruneExcessRequests(WebhookEndpoint $endpoint): void
{
$requestIds = $endpoint->requests()
->select('id')
->orderByDesc('received_at')
->orderByDesc('id')
->skip((int) config('webhooks.max_requests_per_endpoint', 1000))
->take(1_000_000_000)
->pluck('id');
if ($requestIds->isNotEmpty()) {
WebhookRequest::query()->whereKey($requestIds)->delete();
}
}
}