Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Proxy Laravel Package

jenssegers/proxy

Lightweight proxy package for Laravel that forwards incoming HTTP requests to an upstream host and returns the response unchanged. Handy for mirroring APIs, avoiding CORS issues, or routing traffic between services with minimal configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and read the source before you wire anything up.

composer require jenssegers/proxy

Two things to check before writing a line of application code:

  1. The Laravel glue may be in a different package. jenssegers/proxy-laravel historically carries the service provider and the session-backed cookie store; jenssegers/proxy is the client itself. Run composer show jenssegers/proxy jenssegers/proxy-laravel and confirm what auto-discovery actually registers on your Laravel version.
  2. Read src/Proxy.php end to end. It is a thin Guzzle wrapper — one class, a handful of methods (get/post/put/delete), a constructor and some option plumbing. Reading it takes under an hour and is the only reliable documentation you will get. Treat the README's language about "streaming" and "middleware" as aspirational rather than descriptive.

Where to look first: the constructor and the array/option bag it accepts (base URI, headers, timeout, cookie store). That is your entire configuration surface. Everything else — auth, allowlisting, header policy, timeouts, logging — you will be writing yourself.

First use case: a fixed-upstream CORS/key-hiding facade. The one scenario this package is genuinely shaped for is a browser calling your Laravel route so that it never sees the third-party URL or credential.

// config/proxy.php
return [
    'upstreams' => [
        'partner' => [
            'base_uri' => env('PARTNER_API_URL'),   // fixed, config-driven, allowlisted
            'token'    => env('PARTNER_API_TOKEN'),
            'timeout'  => 5,
        ],
    ],
];
// app/Http/Controllers/PartnerOrdersController.php
public function index()
{
    $proxy = app(Proxy::class, [
        'base_uri' => config('proxy.upstreams.partner.base_uri'),
    ]);

    $response = $proxy->get('v1/orders', [
        'headers' => [
            'Authorization' => 'Bearer '.config('proxy.upstreams.partner.token'),
            'Accept'        => 'application/json',
        ],
        'timeout'         => config('proxy.upstreams.partner.timeout'),
        'connect_timeout' => 2,
        'http_errors'     => false,
    ]);

    return response((string) $response->getBody(), $response->getStatusCode())
        ->header('Content-Type', 'application/json');
}

Bind Proxy in a service provider rather than resolving it ad hoc, so there is exactly one place where upstreams, timeouts and header policy are defined. If you find yourself writing new Proxy(...) in a controller, stop.

Do not start with a wildcard route. Route::any('/proxy/{any?}', ...) that reads the destination from the request is an open proxy and an SSRF vector on day one. Model one route per upstream operation.

Implementation Patterns

Config-driven upstream registry, one instance per upstream. Keep a small map of named upstreams with base_uri, credential, timeout and an explicit allowlist of paths or verbs. Resolve the Proxy from that map so the destination is never derived from user input.

Route shape: explicit, verb-mapped, operation-named.

Route::middleware(['auth', 'throttle:30,1'])
    ->prefix('api/partner')
    ->group(function () {
        Route::get('orders', [PartnerOrdersController::class, 'index']);
        Route::post('orders/{id}/cancel', [PartnerOrdersController::class, 'cancel']);
    });

Explicit routes give you auth, throttling and logging for free, and they document the actual upstream contract. A catch-all gives you a security incident.

Forwarding request data — deliberately, field by field. Passing $request->all() or $request->headers->all() straight through is how upstreams get confused and how credentials leak. Build the outbound payload:

  • Query: $request->query() is usually fine, but strip anything used for local routing/pagination that the upstream doesn't understand.
  • Body: $request->validate([...]) first, then forward the validated array. Never forward raw input to a partner.
  • Headers: allowlist. Typically Accept, Content-Type, and maybe Accept-Language. Never forward Cookie, Authorization, Host, X-Forwarded-*, or Content-Length.
  • Credentials: inject server-side and overwrite any inbound Authorization rather than letting it pass through.

Response handling — passthrough is not automatic. You are constructing a new Response. Decide explicitly:

$status = $response->getStatusCode();
$body   = (string) $response->getBody();   // consume once

return response($body, $status)
    ->header('Content-Type', $response->getHeaderLine('Content-Type') ?: 'application/json');
// Do NOT forward Set-Cookie or Location from the upstream.

Rewrite or drop upstream Location headers — otherwise a 302 from the partner will bounce your user to a third-party host. Strip Set-Cookie unless you have a specific, scoped reason to accept it.

Wrap it behind your own interface. The highest-leverage pattern with this package is to never let Jenssegers\Proxy\Proxy appear in more than one file:

interface UpstreamFetcher
{
    public function request(string $upstream, string $method, string $path, array $options = []): Response;
}

Bind a ProxyUpstreamFetcher today, swap in a Guzzle-middleware implementation next quarter with a one-line container change. Given the maintenance signals, this is the difference between a reversible decision and a permanent one.

Testing. Http::fake() will not intercept calls made through Guzzle directly. Either inject a Guzzle handler stack with MockHandler in tests, or — better — fake your own UpstreamFetcher interface and never exercise the library in the test suite at all. The second option also means your tests survive the swap.

Instrument at the wrapper. Use a Guzzle middleware on the handler stack to log upstream name, status, latency and error class. The package emits nothing on its own; without this you have a blind spot in production.

Gotchas and Tips

SSRF is the whole ballgame. The library enforces no allowlist. If the destination URL is (or can be influenced by) the request, you have built an open proxy into your internal network. Keep the base URI in config, resolve the upstream by a named key, and validate that any path segment you interpolate cannot contain .., //, or a scheme.

Redirects are followed by default. Guzzle follows up to 5 redirects unless told otherwise, and a partner-controlled redirect is an SSRF path. Consider 'allow_redirects' => false for anything security-sensitive, or cap and validate.

Default Guzzle timeouts are effectively infinite. Set both timeout and connect_timeout. Without them, a stalled upstream pins a PHP-FPM worker for the full duration of the request — throughput ceiling becomes worker pool ÷ upstream latency. Also cap response size if payloads aren't small and bounded.

Bodies are buffered. (string) $response->getBody() reads the whole thing into memory. Fine for JSON APIs in the low megabytes; an OOM kill for file downloads. Don't proxy file transfers with this.

Header and cookie passthrough is the default footgun. Inbound cookies leaking upstream, upstream Set-Cookie landing in your origin's cookie jar, and missing/altered Host or Content-Length producing opaque upstream 4xx/5xx with no diagnostics. Assert on your outbound header set in a test.

Per-request cookie stores silently break sessions. If you instantiate a fresh Proxy per request with a fresh jar, every upstream call looks like a new visitor. If the upstream needs session continuity, share the jar via the container and scope it per authenticated user — and be explicit that you are now holding upstream session state.

Compatibility is unproven, not implied. v3.1.0 set the floor at PHP 7.1 and there has been no activity since. Prove PHP 8.x and Laravel 9/10/11 in CI rather than assuming, and check composer why-not before working around it with --ignore-platform-reqs (a red flag, not a fix). Audit the companion proxy-laravel package separately — one abandoned dependency is a line item, two is a pattern.

Extension points are where the library ends. There is no policy layer to hook into. Auth injection, allowlisting, timeouts, size caps, header sanitization, retries, observability and caching are all yours to build around it. Budget for that in the estimate; the "install and call get()" path is a day, the production-hardened version is meaningfully more.

Write the decision record. In-repo, next to the wrapper: allowed upstreams, header policy, timeout values, and the reason they exist. Otherwise the next developer "simplifies" the controller, removes the header allowlist, and quietly reintroduces the open proxy.

Plan the exit now. Copy the small amount of logic into app/Support/Proxy behind UpstreamFetcher, or replace it with a ProxyController plus Guzzle middleware, and the abandoned dependency becomes an owned component with a real upgrade path. Do that before the first line reaches production if you can; do it within the first quarter if you can't.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle