Skip to content

Commit b2eb13b

Browse files
committed
Inherit proxy headers
1 parent 418c3d5 commit b2eb13b

File tree

1 file changed

+238
-0
lines changed

1 file changed

+238
-0
lines changed

tests/test_reverse_proxy.py

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,241 @@ async def test_non_standard_port(mock_request):
171171
handler = ReverseProxyHandler(upstream="http://upstream-api.com:8080")
172172
headers = handler._prepare_headers(mock_request)
173173
assert headers["Host"] == "upstream-api.com:8080"
174+
175+
176+
@pytest.mark.asyncio
177+
async def test_nginx_proxy_headers_preserved():
178+
"""Test that existing proxy headers from NGINX are preserved."""
179+
# Simulate a request that already has proxy headers set by NGINX
180+
scope = {
181+
"type": "http",
182+
"method": "GET",
183+
"path": "/test",
184+
"headers": [
185+
(b"host", b"localhost:8000"),
186+
(b"user-agent", b"test-agent"),
187+
(b"x-forwarded-for", b"203.0.113.1, 198.51.100.1"),
188+
(b"x-forwarded-proto", b"https"),
189+
(b"x-forwarded-host", b"api.example.com"),
190+
(b"x-forwarded-path", b"/api/v1"),
191+
],
192+
}
193+
request = Request(scope)
194+
handler = ReverseProxyHandler(upstream="http://upstream-api.com")
195+
headers = handler._prepare_headers(request)
196+
197+
# Check that the existing proxy headers are preserved in the Forwarded header
198+
forwarded = headers["Forwarded"]
199+
assert "for=203.0.113.1, 198.51.100.1" in forwarded
200+
assert "host=api.example.com" in forwarded
201+
assert "proto=https" in forwarded
202+
assert "path=/api/v1" in forwarded
203+
204+
# The original headers should still be present (they're preserved from the request)
205+
assert headers["X-Forwarded-For"] == "203.0.113.1, 198.51.100.1"
206+
assert headers["X-Forwarded-Host"] == "api.example.com"
207+
assert headers["X-Forwarded-Proto"] == "https"
208+
assert headers["X-Forwarded-Path"] == "/api/v1"
209+
210+
211+
@pytest.mark.asyncio
212+
async def test_nginx_proxy_headers_preserved_with_legacy():
213+
"""Test that existing proxy headers from NGINX are preserved with legacy mode."""
214+
# Simulate a request that already has proxy headers set by NGINX
215+
scope = {
216+
"type": "http",
217+
"method": "GET",
218+
"path": "/test",
219+
"headers": [
220+
(b"host", b"localhost:8000"),
221+
(b"user-agent", b"test-agent"),
222+
(b"x-forwarded-for", b"203.0.113.1, 198.51.100.1"),
223+
(b"x-forwarded-proto", b"https"),
224+
(b"x-forwarded-host", b"api.example.com"),
225+
(b"x-forwarded-path", b"/api/v1"),
226+
],
227+
}
228+
request = Request(scope)
229+
handler = ReverseProxyHandler(
230+
upstream="http://upstream-api.com", legacy_forwarded_headers=True
231+
)
232+
headers = handler._prepare_headers(request)
233+
234+
# Check that the existing proxy headers are preserved in both formats
235+
forwarded = headers["Forwarded"]
236+
assert "for=203.0.113.1, 198.51.100.1" in forwarded
237+
assert "host=api.example.com" in forwarded
238+
assert "proto=https" in forwarded
239+
assert "path=/api/v1" in forwarded
240+
241+
# Legacy headers should also be preserved
242+
assert headers["X-Forwarded-For"] == "203.0.113.1, 198.51.100.1"
243+
assert headers["X-Forwarded-Host"] == "api.example.com"
244+
assert headers["X-Forwarded-Proto"] == "https"
245+
assert headers["X-Forwarded-Path"] == "/api/v1"
246+
247+
248+
@pytest.mark.asyncio
249+
async def test_partial_nginx_headers_fallback():
250+
"""Test fallback behavior when only some proxy headers are present."""
251+
# Simulate a request with only some proxy headers set by NGINX
252+
scope = {
253+
"type": "http",
254+
"method": "GET",
255+
"path": "/test",
256+
"headers": [
257+
(b"host", b"localhost:8000"),
258+
(b"user-agent", b"test-agent"),
259+
(b"x-forwarded-for", b"203.0.113.1"),
260+
(b"x-forwarded-proto", b"https"),
261+
# Missing X-Forwarded-Host and X-Forwarded-Path
262+
],
263+
}
264+
request = Request(scope)
265+
handler = ReverseProxyHandler(upstream="http://upstream-api.com")
266+
headers = handler._prepare_headers(request)
267+
268+
# Check that existing headers are preserved and missing ones fall back to request values
269+
forwarded = headers["Forwarded"]
270+
assert "for=203.0.113.1" in forwarded # From existing header
271+
assert "host=localhost:8000" in forwarded # Fallback to request host
272+
assert "proto=https" in forwarded # From existing header
273+
assert "path=/" in forwarded # Fallback to request path
274+
275+
276+
@pytest.mark.asyncio
277+
async def test_nginx_headers_with_client_info():
278+
"""Test that NGINX headers take precedence over client info."""
279+
# Simulate a request with both client info and existing proxy headers
280+
scope = {
281+
"type": "http",
282+
"method": "GET",
283+
"path": "/test",
284+
"client": ("192.168.1.1", 12345), # This should be ignored
285+
"headers": [
286+
(b"host", b"localhost:8000"),
287+
(b"user-agent", b"test-agent"),
288+
(b"x-forwarded-for", b"203.0.113.1, 198.51.100.1"),
289+
],
290+
}
291+
request = Request(scope)
292+
handler = ReverseProxyHandler(upstream="http://upstream-api.com")
293+
headers = handler._prepare_headers(request)
294+
295+
# The existing X-Forwarded-For should take precedence over client info
296+
forwarded = headers["Forwarded"]
297+
assert "for=203.0.113.1, 198.51.100.1" in forwarded
298+
assert "for=192.168.1.1" not in forwarded
299+
300+
301+
@pytest.mark.asyncio
302+
async def test_nginx_headers_with_https_scheme():
303+
"""Test that NGINX headers take precedence over request scheme."""
304+
# Simulate an HTTPS request with existing proxy headers
305+
scope = {
306+
"type": "http",
307+
"method": "GET",
308+
"path": "/test",
309+
"scheme": "https", # This should be ignored
310+
"headers": [
311+
(b"host", b"localhost:8000"),
312+
(b"user-agent", b"test-agent"),
313+
(b"x-forwarded-proto", b"http"), # NGINX says it's HTTP
314+
],
315+
}
316+
request = Request(scope)
317+
handler = ReverseProxyHandler(upstream="http://upstream-api.com")
318+
headers = handler._prepare_headers(request)
319+
320+
# The existing X-Forwarded-Proto should take precedence over request scheme
321+
forwarded = headers["Forwarded"]
322+
assert "proto=http" in forwarded # From existing header
323+
assert "proto=https" not in forwarded
324+
325+
326+
@pytest.mark.asyncio
327+
async def test_nginx_headers_with_custom_path():
328+
"""Test that NGINX headers take precedence over request path."""
329+
# Simulate a request with a custom path and existing proxy headers
330+
scope = {
331+
"type": "http",
332+
"method": "GET",
333+
"path": "/custom/path",
334+
"headers": [
335+
(b"host", b"localhost:8000"),
336+
(b"user-agent", b"test-agent"),
337+
(b"x-forwarded-path", b"/api/v1/root"), # NGINX says different path
338+
],
339+
}
340+
request = Request(scope)
341+
handler = ReverseProxyHandler(upstream="http://upstream-api.com")
342+
headers = handler._prepare_headers(request)
343+
344+
# The existing X-Forwarded-Path should take precedence over request path
345+
forwarded = headers["Forwarded"]
346+
assert "path=/api/v1/root" in forwarded # From existing header
347+
assert "path=/custom/path" not in forwarded
348+
349+
350+
@pytest.mark.asyncio
351+
async def test_nginx_headers_legacy_mode_preservation():
352+
"""Test that NGINX headers are preserved in legacy mode without duplication."""
353+
# Simulate a request that already has proxy headers set by NGINX
354+
scope = {
355+
"type": "http",
356+
"method": "GET",
357+
"path": "/test",
358+
"headers": [
359+
(b"host", b"localhost:8000"),
360+
(b"user-agent", b"test-agent"),
361+
(b"x-forwarded-for", b"203.0.113.1"),
362+
(b"x-forwarded-proto", b"https"),
363+
(b"x-forwarded-host", b"api.example.com"),
364+
(b"x-forwarded-path", b"/api/v1"),
365+
],
366+
}
367+
request = Request(scope)
368+
handler = ReverseProxyHandler(
369+
upstream="http://upstream-api.com", legacy_forwarded_headers=True
370+
)
371+
headers = handler._prepare_headers(request)
372+
373+
# Check that headers are preserved (not duplicated or overwritten)
374+
assert headers["X-Forwarded-For"] == "203.0.113.1"
375+
assert headers["X-Forwarded-Host"] == "api.example.com"
376+
assert headers["X-Forwarded-Proto"] == "https"
377+
assert headers["X-Forwarded-Path"] == "/api/v1"
378+
379+
# Modern Forwarded header should also be present with the same values
380+
forwarded = headers["Forwarded"]
381+
assert "for=203.0.113.1" in forwarded
382+
assert "host=api.example.com" in forwarded
383+
assert "proto=https" in forwarded
384+
assert "path=/api/v1" in forwarded
385+
386+
387+
@pytest.mark.asyncio
388+
async def test_nginx_headers_case_insensitive():
389+
"""Test that NGINX headers are handled case-insensitively."""
390+
# Simulate a request with mixed case proxy headers (some proxies do this)
391+
scope = {
392+
"type": "http",
393+
"method": "GET",
394+
"path": "/test",
395+
"headers": [
396+
(b"host", b"localhost:8000"),
397+
(b"user-agent", b"test-agent"),
398+
(b"X-Forwarded-For", b"203.0.113.1"), # Mixed case
399+
(b"x-forwarded-proto", b"https"), # Lower case
400+
(b"X-FORWARDED-HOST", b"api.example.com"), # Upper case
401+
],
402+
}
403+
request = Request(scope)
404+
handler = ReverseProxyHandler(upstream="http://upstream-api.com")
405+
headers = handler._prepare_headers(request)
406+
407+
# All headers should be preserved regardless of case
408+
forwarded = headers["Forwarded"]
409+
assert "for=203.0.113.1" in forwarded
410+
assert "host=api.example.com" in forwarded
411+
assert "proto=https" in forwarded

0 commit comments

Comments
 (0)