phpmentors/proxy-url-rewrite-bundle
Symfony bundle that rewrites request URLs behind reverse proxies using forwarded headers, helping PHP applications generate correct absolute links, redirects, and assets when served through load balancers or proxy servers.
What it solves. When Symfony runs behind a reverse proxy (nginx, HAProxy, AWS ALB, a CDN), the app sees the internal request (http://10.0.0.5:8080) while users see the public one (https://www.example.com). Anything the framework generates from the request — RedirectResponse targets, $this->generateUrl('route', [], UrlGeneratorInterface::ABSOLUTE_URL), form actions, canonical links, OAuth callback URLs, password-reset links in emails — inherits the internal scheme/host/port. phpmentors/proxy-url-rewrite-bundle exists to make those generated URLs match the public-facing proxy URL.
Install and register.
composer require phpmentors/proxy-url-rewrite-bundle
This is a pre-Symfony-Flex-era bundle (last release 2018-07-30, Symfony 3/4 generation). On Symfony 3 you register it in AppKernel::registerBundles(); on Flex it will usually auto-register, but verify config/bundles.php contains the bundle class. Check the composer.json of the bundle against your Symfony version before committing — if you are on Symfony 5+, the native mechanisms (below) are almost certainly a better fit.
Configure it. Add the bundle's config block (confirm the exact key names in DependencyInjection/Configuration.php — this is a small, low-traffic package, so the source, not blog posts, is the source of truth):
# config/packages/phpmentors_proxy_url_rewrite.yaml
phpmentors_proxy_url_rewrite:
proxy_url: '%env(PROXY_PUBLIC_URL)%' # e.g. https://www.example.com
PROXY_PUBLIC_URL=https://www.example.com
Set it per environment (config/packages/dev/…, config/packages/prod/…) or drive it from an env var so staging and production differ without code changes.
Where to look first, in order.
README.md — install/registration steps and the config sample.DependencyInjection/Configuration.php — the actual, authoritative config keys and whether any are required.EventListener/…Listener.php or Subscriber/…Subscriber.php) — this tells you what it rewrites (request context vs. response headers/body) and at which kernel event, which determines listener ordering.Tests/ — the fastest way to see intended behaviour and edge cases.First use case. Get one redirect working end-to-end. Put the app behind nginx (or a local docker proxy), hit a controller that returns return $this->redirectToRoute('homepage');, and confirm the browser is sent to https://www.example.com/… rather than http://internal-host:8080/…. That single Location header proves the wiring works. Then check a generated absolute URL in a template and a link in an outbound email.
Before you adopt it, know the alternative. Modern Symfony does most of this natively:
// public/index.php
Request::setTrustedProxies(
['127.0.0.1', '10.0.0.0/8'],
Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST
| Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO
);
…plus framework.router.default_uri (Symfony 5.1+) for CLI/worker contexts, and framework.trusted_hosts for host validation. If your proxy already sends correct X-Forwarded-* headers, that combination may cover you entirely. Use this bundle when you cannot rely on headers (e.g. the proxy strips or mangles them) or you need a forced public base URL.
Pattern 1 — Forced public base URL (the main use case). The proxy is opaque and you simply know the public URL. Configure proxy_url statically per environment. This is the lowest-risk configuration: no dependence on which headers the proxy forwards, no trust configuration to get wrong. Use this for redirects, canonical URLs, sitemaps, and email links.
Pattern 2 — Header-driven rewriting. If the proxy sends X-Forwarded-Proto/X-Forwarded-Host, prefer letting Symfony's trusted-proxy support derive the base URL and use the bundle only to force/repair values. Either way, trusted proxies must be configured first — untrusted X-Forwarded-* headers are ignored by Request, and a bundle that reads raw $_SERVER will disagree with the router. Pick one source of truth and document it in the repo.
Pattern 3 — Environment matrix. Keep one config file and vary an env var:
# config/packages/phpmentors_proxy_url_rewrite.yaml
phpmentors_proxy_url_rewrite:
proxy_url: '%env(resolve:PROXY_PUBLIC_URL)%'
# config/packages/prod/services.yaml (or your deployment env)
parameters:
env(PROXY_PUBLIC_URL): 'https://www.example.com'
Never hardcode the production host in a file that is also loaded in dev, or developers will chase redirect loops to production.
Pattern 4 — Generate URLs correctly in the first place. Treat the bundle as a safety net, not a substitute for good URL generation:
$this->generateUrl($route, $params, UrlGeneratorInterface::ABSOLUTE_URL) and Twig's url()/absolute_url() rather than hand-built strings.RedirectResponse built from the router, not new RedirectResponse('https://…').default_uri gotcha below.Anything you build as a raw string bypasses the request context and therefore bypasses the bundle.
Pattern 5 — Proxy configuration that pairs with it. On nginx, so headers and bundle agree:
location / {
proxy_pass http://app:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
If TLS terminates at the proxy and you don't forward X-Forwarded-Proto, you will get http:// URLs on an https:// page — mixed content and, worse, redirect loops on login.
Pattern 6 — Verify in a functional test. The behaviour is a response header, so it is cheap to lock down:
$client = static::createClient();
$client->request('GET', '/login');
$this->assertStringStartsWith('https://www.example.com/', $client->getResponse()->headers->get('Location'));
Run this in CI with the same config as production; it catches a dropped env var instantly.
Pattern 7 — Migration seam. Because this package is small and unmaintained, isolate it: keep the config in one file, avoid referencing its classes from application code, and record the intended end state (Request::setTrustedProxies + default_uri). That way removing it later is a config change rather than a refactor.
composer require resolves, run the full suite before trusting it; if it doesn't resolve, prefer the native approach or copy the listener into your own src/.X-Forwarded-* headers are only honoured for proxies listed via Request::setTrustedProxies(). Without that, Symfony (and any bundle reading the Request) still reports the internal host, and you will conclude "the bundle doesn't work" when the real problem is trust configuration. Set it in public/index.php before the kernel handles the request.bin/console debug:event-dispatcher and check the priority; conflicts with other kernel.request/kernel.response listeners (security, locale, CORS) usually manifest as "works on some routes only".Host and you force proxy_url, you can end up with the public host appended twice or a mismatched Location. Pick one mechanism; don't let both the proxy and the bundle rewrite.path() and relative generateUrl output are correct by definition. If you only test relative links you'll see "it works" and miss that absolute URLs are still wrong. Grep for absolute URL generation and RedirectResponse when auditing.new RedirectResponse('/x') is fine (relative); 'https://'.$request->getHost().'/x' is not. Search the codebase for getHost(), getSchemeAndHttpHost(), and string-concatenated URLs.UrlGenerator in a console command or Messenger worker uses framework.router.request_context (router.request_context.host, .scheme, .base_url) or, on Symfony 5.1+, framework.router.default_uri. A request-scoped bundle cannot help here — this is the single most common "the links in my emails point to localhost" bug, and it is not this bundle's job.framework.session.cookie_domain or framework.trusted_hosts don't match the public host, you get login failures and CSRF mismatches that look like bundle bugs. Align them before debugging the bundle.framework.assets.base_urls / packages/*.base_url / CDN prefixes are generated outside the router context. If asset URLs are still internal after setup, that's why.bin/console debug:config phpmentors_proxy_url_rewrite (or inspect the container) to confirm the value actually loaded; (2) temporarily dump $request->getSchemeAndHttpHost() and the router's RequestContext in a controller; (3) inspect the raw response headers (curl -I) rather than the rendered page; (4) debug:event-dispatcher to confirm the listener is attached.composer.json, and if the package is load-bearing, vendor the listener into your own namespace — it is small enough to own outright and removes an unmaintained dependency from your upgrade path.vendor/. Compiler passes and service decoration work normally as long as the service ID is stable — another reason to read the bundle's Resources/config/services.xml first.How can I help you explore Laravel packages today?