Someone points a public HAProxy host at an on-premises application and asks why the browser cannot follow. The proxy reaches the app through its VPN, the redirect returns a Location header, and the browser then tries an address that exists only inside the tunnel. From the outside the page simply hangs. The decision is about which party opens the second connection, and that is what separates a redirect from a backend.

Two paths that look similar in the configuration

A redirect ends the exchange with a response. The client reads Location and starts a fresh request from wherever the client happens to sit, which is the network you never controlled. The proxy is no longer involved. Everything that follows depends on the client's own DNS, route and trust, so the outcome varies by where the user opened the browser:

GET /app        client -> proxy
302 Location    proxy  -> client   (http://10.40.0.20:8080/)
GET /app        client -> 10.40.0.20:8080   client network, no route

A proxy backend keeps the second leg on the server side. The proxy opens it over the path it already has, and the client keeps talking to one public name. If the tunnel drops, the client receives an error instead of a URL it cannot follow. That difference is also what makes the failure diagnosable, because the error appears where the logs are:

GET /app        client -> proxy
GET /app        proxy  -> 10.40.0.20:8080   over the VPN
response        proxy  -> client

HAProxy's redirect documentation draws exactly this line: answering the client with a redirect is a different action from forwarding the request to a backend. The two directives sit a few lines apart in a configuration file and behave nothing alike. One ends the request, and the other begins a second one that the proxy owns.

What the browser needs that the server's VPN does not give it

The browser's own request needs four things it may not have: a DNS answer for the private name, a route to that address, permission to reach the listening service, and trust for whatever certificate that service presents. The proxy host possesses all four through its tunnel. None of them transfer to a client that was never on that tunnel, because the tunnel is a property of the host, not of the URL.

That gap explains the almost-works symptom. The first request succeeds, the address bar changes, and the connection then hangs or fails while the proxy is healthy the whole time. Nothing is broken in the proxy configuration, and adjusting it cannot help a client that has no path to 10.40.0.20. Either the client gets a route, or the proxy has to make the connection on its behalf.

When only the proxy can reach the app

Remove the redirect action and send those requests to a backend that points at the private address. The frontend keeps its public name while the backend decides where the request actually goes. Nothing about the client's path changes, because the proxy stops describing a destination and starts being one. For an application deliberately listening on internal port 8080, the server block is short:

backend private_app
    mode http
    option httpchk GET /health
    server app1 10.40.0.20:8080 check

Read that block as a statement about the proxy's routing table: 10.40.0.20 must be reachable from the proxy host itself, which here means through the VPN interface rather than the default route. The check keyword only reports what that route does, and the health path has to exist on the application or every probe marks the server down. A passing check proves nothing about the client and does not replace the policy that decides who may use the backend.

The fragment is not a complete frontend. Keep the listener, the authentication, the allowed host and path selection, and the rule that decides who may reach the application at all. Match the backend protocol as well, because an HTTPS backend needs certificate verification and a server identity configuration, and skipping that check converts a private connection into an unverified one.

Confirm the return path in both directions before declaring victory. The application now sees a connection from the proxy, so its logs and any client-address metadata describe the proxy unless you arrange otherwise. Decide how identity and forwarded addresses are handled, and test the decision rather than assuming it.

The app's own redirects still point inside

Forwarding the first request is only half of the integration. The application may emit absolute redirects, hard-coded asset URLs, or cookies scoped to the name it knows internally, and each of those sends the browser back to an address it cannot reach. A Location header pointing at the internal name looks identical to the original problem, which is why it gets misdiagnosed as a proxy failure.

Set the application's documented external URL and its proxy options, then make the Host header agree with the virtual host and certificate expectations. A cookie scoped to the internal name will not come back through the public name, and the sign-in loop that follows reads as an authentication bug. Change the external URL first, retest from a clean browser profile, and expect half-migrated settings to produce a login that succeeds and a session that dies immediately.

Trust forwarded metadata only from the designated proxy. A header that any client can set must never become an authorization value, or the proxy turns into a way for a caller to claim somebody else's identity. That rule is independent of the redirect question and survives every later redesign.

Decide which design you actually have

If every user already has authorized VPN access, a redirect to a private URL can be a deliberate design. Confirm that requirement explicitly instead of assuming it, because the two designs serve completely different populations. One of them does not work at all from a hotel network, and the other exposes the application to anyone who can load the public URL. That distinction belongs in the design note.

Run the check from a machine on the users' network and follow every hop the browser makes, not just the first response. Keep an eye on the address bar and on each subsequent request, because a single absolute URL in an asset tag is enough to break a flow that the first hop passed. Sign-in has to work through the public name, and resources that should be denied have to stay denied.

Publishing a reverse proxy is a separate access decision, not a transparent extension of the proxy's own VPN to anyone who can load its public URL. The two look alike from a diagram, and they differ in who is allowed in. Deciding that question after the deployment is how an internal application becomes a public one by accident.

Write down which one you built and why. The next person to touch the configuration will assume the other. A single redirect added for convenience is enough to recreate the original failure, and the symptom will look exactly like the one you just fixed.

Erthyglow kevrennek