Looking to hire Laravel developers? Try LaraJobs

laravel-gateway maintained by wuwx

Description
A lightweight HTTP gateway proxy for Laravel applications.
Author
wuwx
Last update
2026/07/26 21:11 (dev-main)
License
Downloads
12
Tags

Comments
comments powered by Disqus

Laravel Gateway

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 — handlers receive the current Request
  • Streaming responses enabled by default (SSE / chunked transfer)
  • Per-route request and response transformation hooks
  • Global and per-handler Guzzle options (timeout, headers, etc.)
  • 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
{
    $provider = request()->attributes->get('gateway.provider');

    return ['base_uri' => $provider->base_url.'/v1/chat/completions'];
}

This is useful when the upstream is determined by middleware, database lookup, or request content.

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' => [
        'strip_client_headers' => ['cookie', 'proxy-authorization'],
        'hop_by_hop_headers' => [
            'host', 'connection', 'transfer-encoding', 'upgrade',
            'keep-alive', 'te', 'trailer', 'proxy-authenticate',
            'proxy-authorization',
        ],
    ],
];

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.