wuwx/laravel-gateway

A lightweight HTTP gateway proxy for Laravel applications.

Maintainers

Package info

github.com/wuwx/laravel-gateway

pkg:composer/wuwx/laravel-gateway

Transparency log

Fund package maintenance!

wuwx

Statistics

Installs: 12

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-07-27 07:03 UTC

This package is auto-updated.

Last update: 2026-07-27 07:03:27 UTC


README

Latest Version on Packagist GitHub Tests Action Status Total Downloads

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_uri resolution via the request() 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.