Laravel Request Forwarder allows you to forward incoming requests to another addresses.
moneo/laravel-request-forwarder is a Laravel package for laravel request forwarder allows you to forward incoming requests to another addresses..
It currently has 81 GitHub stars and 549 downloads on Packagist (latest version v2.0.1).
Install it with composer require moneo/laravel-request-forwarder.
Discover more Laravel packages by moneo
or browse all Laravel packages to compare alternatives.
Last updated

Forward incoming HTTP requests to multiple destinations -- asynchronously, reliably, and with zero config overhead.
Some webhook providers only allow a single callback URL. This package sits behind that URL and fans the request out to as many targets as you need -- different servers, Slack, Discord, or any custom destination -- all processed through Laravel's queue system with automatic retries and failure logging.
WebhookSent and WebhookFailed events let you build dashboards, alerts, or audit logs.tries and exponential backoff per queue job, with failure logging out of the box.
Install the package via Composer:
composer require moneo/laravel-request-forwarder
Publish the configuration file:
php artisan vendor:publish --tag="request-forwarder-config"
1. Set your target URL in .env:
REQUEST_FORWARDER_DEFAULT_URL=https://your-target.com/webhook
2. Add the middleware to any route you want to forward:
Route::middleware('request-forwarder')
->post('/webhook', fn () => response()->json(['status' => 'ok']));
That's it. Every request hitting /webhook is now forwarded to your target asynchronously via the queue.
After publishing, the config lives at config/request-forwarder.php:
return [
'default_webhook_group_name' => 'default',
'webhooks' => [
'default' => [
'targets' => [
[
'url' => env('REQUEST_FORWARDER_DEFAULT_URL', 'https://example.com/webhook'),
'method' => 'POST',
],
],
],
],
'timeout' => 30,
'queue_name' => env('REQUEST_FORWARDER_QUEUE', ''),
'queue_class' => Moneo\RequestForwarder\ProcessRequestForwarder::class,
'tries' => 3,
'backoff' => [5, 30, 60],
'log_failures' => true,
];
Key-by-key breakdown:
default_webhook_group_name -- Which webhook group to use when the middleware is called without a parameter.webhooks -- A map of named groups. Each group contains a targets array. Every target needs at least a url. Optional keys per target: method (default POST), provider, headers, timeout.timeout -- Global HTTP timeout in seconds for outgoing requests. Can be overridden per target.queue_name -- The queue connection/name for async jobs. Leave empty to use Laravel's default queue.queue_class -- The job class used for dispatching. Override this if you need custom job logic.tries -- How many times a failed job is retried before being marked as permanently failed.backoff -- Seconds to wait between retries. Accepts a single integer or an array for progressive backoff.log_failures -- When true, failed deliveries are written to your Laravel log.Attach the request-forwarder middleware to any route. The middleware dispatches a queue job and lets the request continue normally -- your users see no delay.
// Forward using the default webhook group
Route::middleware('request-forwarder')
->post('/webhook', fn () => 'OK');
// Forward using a named webhook group
Route::middleware('request-forwarder:payments')
->post('/payments/webhook', fn () => 'OK');
Use the RequestForwarder facade when you need more control:
use Moneo\RequestForwarder\Facades\RequestForwarder;
// Dispatch to queue (async)
RequestForwarder::sendAsync($request);
RequestForwarder::sendAsync($request, 'payments');
// Trigger immediately (sync) -- useful in jobs or artisan commands
RequestForwarder::triggerHooks('https://original-url.com/hook', ['key' => 'value']);
RequestForwarder::triggerHooks('https://original-url.com/hook', $data, 'payments');
Define as many groups as you need. Each group is independently configurable:
'webhooks' => [
'default' => [
'targets' => [
['url' => 'https://primary-backend.com/hook', 'method' => 'POST'],
],
],
'payments' => [
'targets' => [
['url' => 'https://accounting.internal/stripe', 'method' => 'POST'],
[
'url' => 'https://discord.com/api/webhooks/...',
'method' => 'POST',
'provider' => \Moneo\RequestForwarder\Providers\Discord::class,
],
],
],
],
Add authentication or custom headers to individual targets:
'targets' => [
[
'url' => 'https://api.partner.com/webhook',
'method' => 'POST',
'headers' => [
'Authorization' => 'Bearer your-api-token',
'X-Webhook-Secret' => 'shared-secret',
],
],
],
Override the global timeout for slow endpoints:
'targets' => [
[
'url' => 'https://slow-service.example.com/hook',
'method' => 'POST',
'timeout' => 60,
],
],
The default provider sends JSON over HTTP. Need a different format? Implement ProviderInterface:
use Illuminate\Http\Client\Factory;
use Illuminate\Http\Client\Response;
use Moneo\RequestForwarder\Providers\ProviderInterface;
class SlackProvider implements ProviderInterface
{
public function __construct(private readonly Factory $client)
{
}
public function send(string $url, array $params, array $webhook): Response
{
return $this->client
->timeout($webhook['timeout'] ?? 30)
->send('POST', $webhook['url'], [
'json' => [
'text' => "Webhook from {$url}\n```" . json_encode($params, JSON_PRETTY_PRINT) . '```',
],
]);
}
}
Register your provider in the target config:
'targets' => [
[
'url' => 'https://hooks.slack.com/services/T.../B.../xxx',
'method' => 'POST',
'provider' => App\Webhooks\SlackProvider::class,
],
],
The package validates that every provider class exists and implements ProviderInterface before instantiation. Providers are resolved through Laravel's container, so constructor injection works out of the box.
Every delivery attempt dispatches an event you can hook into:
WebhookSent -- Dispatched after a successful HTTP response (any status code).
string $sourceUrl -- The original incoming request URL.string $targetUrl -- The target the request was forwarded to.int $statusCode -- The HTTP status code returned by the target.WebhookFailed -- Dispatched when the delivery throws any exception.
string $sourceUrl -- The original incoming request URL.string $targetUrl -- The target that failed.\Throwable $exception -- The exception that was caught.Example listener:
use Moneo\RequestForwarder\Events\WebhookSent;
use Moneo\RequestForwarder\Events\WebhookFailed;
// In a service provider or event subscriber
Event::listen(WebhookSent::class, function (WebhookSent $event) {
logger()->info("Forwarded to {$event->targetUrl}", [
'status' => $event->statusCode,
]);
});
Event::listen(WebhookFailed::class, function (WebhookFailed $event) {
logger()->error("Forward to {$event->targetUrl} failed", [
'error' => $event->exception->getMessage(),
]);
});
The middleware dispatches a ProcessRequestForwarder job. Configure its behavior in the config:
'queue_name' => env('REQUEST_FORWARDER_QUEUE', 'webhooks'),
'tries' => 3,
'backoff' => [5, 30, 60], // wait 5s, then 30s, then 60s
queue_name is empty, Laravel's default queue is used.tries and backoff are validated at runtime; invalid values fall back to safe defaults (3 tries, [5, 30, 60] backoff).Custom job class: If you need to customize serialization, middleware, or tagging, extend the default job and point the config to your class:
// app/Jobs/CustomForwarder.php
class CustomForwarder extends \Moneo\RequestForwarder\ProcessRequestForwarder
{
public $timeout = 120;
public function tags(): array
{
return ['webhook-forwarder', "group:{$this->webhookName}"];
}
}
// config/request-forwarder.php
'queue_class' => App\Jobs\CustomForwarder::class,
The package is designed to never break your application flow:
Inside triggerHooks: Each target is processed independently. If one target fails, the remaining targets still execute. Failed targets dispatch a WebhookFailed event and (when log_failures is true) write an error to your Laravel log.
Queue job failures: When all retries are exhausted, the job's failed() method logs the final error with the source URL and webhook group name.
Strict validation: Invalid URLs, unsupported HTTP methods, non-positive timeouts, non-existent provider classes, and malformed config shapes all throw InvalidArgumentException eagerly -- so misconfigurations surface during development, not in production.
8.2+ (was 8.1+).11.x or 12.x (Laravel 10 support dropped; it reached EOL in February 2025).v2.0 fails fast on invalid configuration instead of silently ignoring it. Check your webhook groups:
targets must be a non-empty array.url (passes FILTER_VALIDATE_URL).method must be one of: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS.timeout must be a positive number.headers must be an array of string keys with scalar values.ProviderInterface.WebhookFailed instead of being silently skipped.queue_name no longer forces an empty string -- it falls back to Laravel's default queue.tries and backoff are validated and normalized at runtime.WebhookFailed::$exception is now \Throwable (was \Exception).composer require moneo/laravel-request-forwarder:^2.0php artisan vendor:publish --tag="request-forwarder-config" --forceInvalidArgumentException entries from the package.# Run the test suite
composer test
# Run static analysis
composer analyse
# Fix code style
composer format
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.