wuwx / laravel-gateway
A lightweight HTTP gateway proxy for Laravel applications.
Fund package maintenance!
Requires
- php: ^8.3
- illuminate/contracts: ^11.0||^12.0||^13.0
- illuminate/http: ^11.0||^12.0||^13.0
- illuminate/routing: ^11.0||^12.0||^13.0
- illuminate/support: ^11.0||^12.0||^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^11.0.0||^10.0.0||^9.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
This package is auto-updated.
Last update: 2026-07-27 07:03:27 UTC
README
A lightweight HTTP gateway proxy for Laravel applications. Define proxy routes with a familiar routing syntax, transform requests and responses per-route, and stream upstream responses back to clients — all within Laravel's native routing and middleware system.
Features
- Route-based proxy definitions using
Gateway::get/post/put/patch/delete/any - Dynamic
base_uriresolution via therequest()helper - Streaming responses enabled by default (SSE / chunked transfer)
- Per-route request and response transformation hooks
- Global and per-handler Guzzle options (timeout, headers, etc.)
- Lifecycle events:
GatewayRequestSending,GatewayResponseReceived,GatewayRequestFailed - Works with Laravel route groups, middleware, named routes, and
route:list - Secure by default: hop-by-hop and sensitive client headers stripped
- Path traversal and scheme injection protection
Requirements
- PHP 8.4+
- Laravel 11.x, 12.x, or 13.x
Installation
composer require wuwx/laravel-gateway
Publish the config file:
php artisan vendor:publish --tag="gateway-config"
Usage
Defining proxy routes
Gateway routes are defined in your route files using the Gateway facade:
use Wuwx\LaravelGateway\Facades\Gateway; Gateway::post('v1/chat/completions', ChatHandler::class); Gateway::any('v1/{path}', CatchAllHandler::class);
Each call registers a real Laravel route, so proxy routes appear in php artisan route:list.
The {path} parameter captures the sub-path forwarded to the upstream service:
POST /v1/chat/completions → POST https://api.openai.com/v1/chat/completions
GET /proxy/users/123 → GET https://api.example.com/users/123
Route groups & middleware
Gateway routes work inside any Laravel route group:
Route::prefix('v1')->middleware(['auth:sanctum', 'throttle:api'])->group(function () { Gateway::any('chat/completions', AiHandler::class); Gateway::any('embeddings', AiHandler::class); });
Route chaining also works:
Gateway::post('orders/{path}', OrderHandler::class) ->middleware('throttle:60,1') ->name('orders.proxy');
Creating a handler
Every Gateway route requires a handler class. Extend GatewayHandler for no-op defaults:
use Illuminate\Http\Request; use Wuwx\LaravelGateway\GatewayHandler; class OpenAiHandler extends GatewayHandler { public function getOptions(): array { return [ 'base_uri' => 'https://api.openai.com/v1', 'timeout' => 120, ]; } public function getRequest(Request $request): Request { $request->headers->set('Authorization', 'Bearer '.config('services.openai.key')); return $request; } }
The three interface methods:
| Method | Purpose |
|---|---|
getOptions(): array |
Upstream base_uri and Guzzle options. Use request() for dynamic resolution. |
getRequest(Request $request): Request |
Transform the request before forwarding (inject credentials, rewrite headers). |
getResponse(Request $request, Response $response): Response |
Transform the upstream response before returning to the client. Not called for streamed responses. |
If you prefer to implement the interface directly, implement GatewayControllerInterface instead of extending GatewayHandler.
Dynamic base_uri
Use the request() helper inside getOptions() to resolve the upstream target dynamically:
public function getOptions(): array { $model = AiModel::where('external_model_id', request()->json('model'))->firstOrFail(); return ['base_uri' => rtrim($model->provider->base_url, '/')]; }
This is useful when the upstream is determined by database lookup or request content. The request path is forwarded as-is — Guzzle's base_uri mechanism handles URL resolution (RFC 3986).
Streaming
Streaming is enabled by default. Responses are forwarded chunk-by-chunk via StreamedResponse, making it ideal for SSE and large payloads.
Disable streaming globally:
GATEWAY_STREAMING=false
Or per-handler:
public function getOptions(): array { return [ 'base_uri' => 'https://api.example.com/v1', 'streaming' => false, ]; }
When streaming is disabled, the full response is buffered and the getResponse() hook is called.
Configuration
// config/gateway.php return [ 'enabled' => env('GATEWAY_ENABLED', true), 'options' => [ 'timeout' => env('GATEWAY_TIMEOUT', 30), 'connect_timeout' => env('GATEWAY_CONNECT_TIMEOUT', 10), 'http_errors' => false, ], 'streaming' => env('GATEWAY_STREAMING', true), 'security' => [ 'enforce_allowed_hosts' => env('GATEWAY_ENFORCE_ALLOWED_HOSTS', false), 'allowed_hosts' => [], 'strip_client_headers' => ['cookie', 'proxy-authorization'], 'hop_by_hop_headers' => [ 'host', 'connection', 'transfer-encoding', 'upgrade', 'keep-alive', 'te', 'trailer', 'proxy-authenticate', 'proxy-authorization', ], ], 'cache' => [ 'store' => env('GATEWAY_CACHE_STORE', null), ], 'events' => [ 'enabled' => env('GATEWAY_EVENTS', true), ], ];
Events
When gateway.events.enabled is true (default), Gateway dispatches three events around each proxy call:
| Event | Dispatched | Payload |
|---|---|---|
GatewayRequestSending |
Before the upstream call | $request, $handlerClass, $url, $startedAt |
GatewayResponseReceived |
After a successful response | $request, $response, $handlerClass, $durationMs |
GatewayRequestFailed |
When the upstream call throws | $request, $handlerClass, $exception, $durationMs |
Listen for them in your EventServiceProvider or via auto-discovery:
Event::listen(GatewayResponseReceived::class, function ($event) { Log::info('Gateway proxy completed', [ 'handler' => $event->handlerClass, 'duration_ms' => $event->durationMs, ]); });
Security
Header stripping
Sensitive client headers (cookie, proxy-authorization) are stripped before forwarding. Hop-by-hop headers (RFC 7230) are always removed from both request and response.
Handlers can re-add credentials from your own config inside getRequest():
public function getRequest(Request $request): Request { $request->headers->set('Authorization', 'Bearer '.config('services.github.token')); return $request; }
Path protection
Gateway rejects paths containing traversal segments (../, ./) or scheme/authority injection (http://, //) with a 400 response before any upstream call is made.
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.