tourze/http-forward-bundle
Tourze HTTP Forward Bundle is a Symfony bundle for PHP that forwards incoming HTTP requests to another backend, making it easy to build lightweight proxies, request relays, and traffic redirects with simple configuration and minimal code.
tourze/http-forward-bundle is a request-forwarding layer: it takes an incoming HTTP request, replays it against an upstream/backend URL, and returns the upstream response to the caller. In day-to-day terms it's the plumbing behind a proxy endpoint, a strangler-fig migration off a legacy service, or a way to expose a third-party API under your own domain without building a controller per endpoint.
Version context: 1.0.1 is a patch release. The notes contain only the compare link (1.0.0...1.0.1), so there are no declared breaking changes, new features, or deprecations. Treat it as a safe bump; pin ^1.0 and skim the diff at the compare URL before upgrading if you depend on internals.
Install:
composer require tourze/http-forward-bundle
Important framing: this is a Symfony bundle, not a Laravel package. There is no service provider, no config/http-forward.php, and no facade. If you're in a Laravel codebase you have two realistic options:
Where to look first, in order:
src/ — find the class that actually performs the forward (usually named something like *Forwarder, *Client, or *Proxy). That's the seam you'll interact with.DependencyInjection/Configuration.php — the authoritative list of config keys and defaults. There is no README substitute for this file.Resources/config/services.php (or .yaml) — the service IDs and constructor arguments, which tells you what to bind/override.composer.json — confirm the PSR-7/PSR-18/PSR-15 interfaces and the Symfony version constraints before assuming Laravel compatibility.First use case: a catch-all route that proxies /api/legacy/{any?} to a legacy backend during migration.
// Laravel routes/api.php
Route::any('/legacy/{any?}', LegacyProxyController::class)->where('any', '.*');
final class LegacyProxyController
{
public function __construct(private Forwarder $forwarder) {}
public function __invoke(Request $request)
{
return $this->forwarder->forward(
$request,
config('services.legacy.base_url').'/'.$request->path()
);
}
}
Start with a single read-only GET endpoint before forwarding anything that mutates state.
Method and payload fidelity. The forwarder's value is that it preserves the verb, headers, query string, and body. Build your wrapper so nothing narrows that: pass the raw body through rather than $request->all() re-encoded as JSON, since re-encoding breaks multipart uploads and changes numeric/boolean typing.
Strip hop-by-hop headers before forwarding. Host, Connection, Keep-Alive, Transfer-Encoding, Content-Length, Upgrade, and TE describe the client↔your-app connection, not the upstream one. Leaking Content-Length after re-encoding the body is the single most common cause of hangs and truncated responses. Set a fresh Host for the upstream.
Auth handling is a decision, not a default. Three patterns, pick deliberately:
Authorization untouched. Correct when the upstream validates the same token.Never blindly forward Cookie. If both apps share a session cookie name you'll get confusing cross-authentication behaviour.
Stream, don't buffer. For file downloads and large JSON payloads, ensure the response body is streamed (StreamedResponse / a PSR-7 stream) rather than read into a string. A 200 MB export forwarded into memory will OOM the worker under any concurrency.
Timeouts and failure mapping. Configure a connect timeout and a total timeout on the underlying HTTP client. When the upstream is down or slow, return a clean 502 / 504 with a minimal body — never let a Guzzle/Symfony HTTP exception bubble into a rendered stack trace, which can leak the upstream hostname and internal paths.
Correlation IDs. Generate or accept an X-Request-Id, attach it to the outbound request, and log it alongside the inbound request. When a user reports a failure you need to find both sides of the call in one query.
Laravel integration shape. Keep the bundle behind a small interface so you're not importing third-party classes across your codebase:
// app/Contracts/UpstreamForwarder.php
interface UpstreamForwarder
{
public function forward(Request $request, string $targetUrl): Response;
}
Bind the bundle's class in a service provider (or an adapter that translates Laravel's Request/Response into the PSR/Symfony equivalents). This costs ~30 lines and means a future version bump or package swap touches one file.
Testing. In Laravel, Http::fake() covers the PSR-18 client only if the bundle is configured to use it — otherwise you'll need to fake at the boundary of your adapter. Assert on the outbound URL, method, and headers; Http::assertSent(fn ($req) => $req->url() === ...). Add one test per behaviour that matters: method preservation, query string encoding, header stripping, upstream 4xx passthrough, upstream timeout mapping.
Route design. A {any?} catch-all is convenient but it also swallows routes you later want to define locally. Scope it under a distinct prefix (/legacy/..., /proxy/...) and register it last so explicit routes win.
The empty changelog is itself the finding. A patch release with only a compare link means you cannot reason about the change from the notes. Read the diff at the linked URL before upgrading in production — "patch" is a versioning claim, not a guarantee. Watch for changed default config values or bumped dependency constraints, both of which ship happily in patch versions.
Symfony bundle ≠ Laravel package. Do not run php artisan vendor:publish and expect anything. Configuration lives in the bundle's Configuration.php and is normally loaded through a Symfony kernel extension. In Laravel you're responsible for instantiating and configuring the object graph yourself. If the constructor expects Symfony services (event dispatcher, HTTP client, logger), you'll need to supply Laravel equivalents or PSR-compatible ones.
Query string fidelity. Rebuilding the query from $request->query() and http_build_query() will re-encode arrays and change key ordering, and it drops repeated keys in some cases. Append the raw QUERY_STRING server value to the target URL instead. Symptom to look for: upstream 400s that only occur on requests with nested or repeated parameters.
Don't follow redirects. Set allow_redirects => false on the client. A 302 from the upstream is a legitimate response for the caller; following it internally silently changes the semantics and can mask an auth flow the client needed to see.
Forward-loop protection. A misconfigured base URL pointing back at your own host produces an infinite recursion that manifests as a worker timeout, not an error. Assert at boot that the upstream host differs from APP_URL's host, or add a hop-count header check.
Trusted proxies and X-Forwarded-*. Once your app forwards, there are now two proxy hops. Laravel's TrustProxies middleware and Symfony's trusted_proxies config must both agree, or $request->ip() and generated URLs will be wrong — and any rate limiting keyed on IP will misbehave.
Debugging checklist when the upstream rejects you: log the exact outbound URL, method, and full header set as a single structured entry before the call. Nine times out of ten the problem is a stale Host header, a duplicated Content-Type, or a Content-Length from the original request. A raw curl reproduction using the logged values isolates package bug from config bug in under a minute.
Extension points worth knowing:
Upgrade discipline. Given a 1.0.x version and a thin changelog, pin to ~1.0.1 rather than ^1.0 if the forwarding path is on a critical route, and re-check the compare link on each release. Keep your adapter thin — that's the real risk mitigation here, more than any version constraint.
How can I help you explore Laravel packages today?