From 199cd3607563dc105cff2661bc9087c7a82e3227 Mon Sep 17 00:00:00 2001 From: Steve Purcell Date: Wed, 14 Oct 2026 17:25:10 +0100 Subject: [PATCH] Proxy: always add X-Forwarded-Proto alongside X-Forwarded-For This header is used by convention to allow downstream servers to reconstruct the original request URL, even if that request to the proxy server was made via a different protocol from that on which the downstream is listening. A typical use case is when the proxy server listens on https but the downstream backend listens on http. We only add this header if not provided by a (further) upstream proxy, and if not explicitly overridden, e.g. by the module-level "forward()" method. --- rolo/proxy.py | 3 +++ tests/test_proxy.py | 24 +++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/rolo/proxy.py b/rolo/proxy.py index c5d125c..b704951 100644 --- a/rolo/proxy.py +++ b/rolo/proxy.py @@ -75,6 +75,9 @@ def forward( else: headers["X-Forwarded-For"] = f"{client_ip}" + if "X-Forwarded-Proto" not in request.headers and "X-Forwarded-Proto" not in headers: + headers["X-Forwarded-Proto"] = f"{request.scheme}" + if forward_path is None: forward_path = get_raw_path(request) if forward_path: diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 62a09e0..89e83f2 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -111,7 +111,7 @@ def test_get_with_different_base_url_plain_rule(self, router_server, httpserver: assert response.ok assert response.text == "ok/bar/" # it's calling /bar/ because it's part of the root URL - def test_xff_header(self, router_server, httpserver: HTTPServer): + def test_x_forwarded_for_header(self, router_server, httpserver: HTTPServer): router, proxy = router_server backend = httpserver @@ -133,6 +133,28 @@ def _echo_headers(request): headers = response.json() assert headers["X-Forwarded-For"] == "127.0.0.2, 127.0.0.1" + def test_x_forwarded_proto_header(self, router_server, httpserver: HTTPServer): + router, proxy = router_server + backend = httpserver + + def _echo_headers(request): + return Response(json.dumps(dict(request.headers)), mimetype="application/json") + + backend.expect_request("/echo").respond_with_handler(_echo_headers) + + router.add("/", ProxyHandler(backend.url_for("/"))) + + response = requests.get(proxy.url + "/echo") + assert response.ok + headers = response.json() + assert headers["X-Forwarded-Proto"] == "http" + + # Any existing X-Forwarded-Proto header should not be overwritten + response = requests.get(proxy.url + "/echo", headers={"X-Forwarded-Proto": "https"}) + assert response.ok + headers = response.json() + assert headers["X-Forwarded-Proto"] == "https" + def test_post_form_data_with_query_args(self, router_server, httpserver: HTTPServer): router, proxy = router_server backend = httpserver