Nginx Proxy Example
Learn how to test Nginx reverse proxy configurations with header transformation, upstream forwarding, and exact destination validation.
Overview
This example tests an Nginx reverse proxy that:
- Routes requests to different endpoints
- Transforms and forwards headers to upstream services
- Handles static HTML and JSON responses
- Proxies dynamic requests to backend services
- Validates request header propagation
- Ensures exact upstream destination targeting
Use Case
Perfect for:
- Reverse proxy configuration testing
- Header transformation validation
- Load balancer testing
- API gateway proxy behavior
- Nginx configuration regression testing
- Upstream service routing validation
Architecture
Client → Nginx (Reverse Proxy) → Mock Backend Service
├── Static responses (/, /api/hello, /api/status)
└── Proxy to upstream (/proxy/*)File Structure
nginx-proxy/
├── Dockerfile
├── nginx.conf
└── .httptests/
├── config.yml
└── test.jsonConfiguration Files
Dockerfile
FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]nginx.conf
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name _;
# Static HTML response
location = / {
default_type text/html;
return 200 '<!DOCTYPE html>
<html>
<head><title>Nginx Proxy</title></head>
<body>
<h1>Nginx Reverse Proxy Example</h1>
<p>This is a static response from Nginx.</p>
</body>
</html>';
}
# Static JSON endpoint
location = /api/hello {
default_type application/json;
return 200 '{"message": "Hello from Nginx proxy!"}';
}
# Status endpoint
location = /api/status {
default_type application/json;
return 200 '{"status": "ok", "proxy": "nginx", "version": "1.0"}';
}
# Proxy endpoints - forward to backend with header transformation
location /proxy/ {
proxy_pass http://backend:5001/;
# Forward client information
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Preserve host
proxy_set_header Host $host;
# Forward all incoming headers
proxy_pass_request_headers on;
# Additional proxy settings
proxy_http_version 1.1;
proxy_set_header Connection "";
}
# Default - not found
location / {
default_type application/json;
return 404 '{"error": "Not found"}';
}
}
}Key features:
/returns HTML directly/api/helloand/api/statusreturn JSON directly/proxy/*forwards tobackend:5001with header transformation- Standard proxy headers: X-Real-IP, X-Forwarded-For, X-Forwarded-Proto
- Host header preservation
.httptests/config.yml
mock:
additional_ports:
- 5001
network_aliases:
- backend
nginx:
environment:
PROXY_VERSION: v1Configuration breakdown:
additional_ports: [5001]- Mock service accessible on port 5001network_aliases: [backend]- Service reachable asbackendin Docker network- Environment variable for the Nginx container
.httptests/test.json
{
"collectionHeaders": [
["X-Request-Id"],
["X-Path"],
["X-Verb"],
["X-QS"]
],
"hosts": {
"example.localhost": [
{
"paths": ["/"],
"method": "GET",
"expectedStatus": 200,
"expectedResponseHeaders": [
["Content-Type", "text/html"]
]
},
{
"paths": ["/api/hello"],
"method": "GET",
"expectedStatus": 200,
"expectedResponseHeaders": [
["Content-Type", "application/json"]
]
},
{
"paths": ["/api/status"],
"method": "GET",
"expectedStatus": 200,
"expectedResponseHeaders": [
["Content-Type", "application/json"]
]
},
{
"paths": ["/proxy/test"],
"method": "GET",
"expectedStatus": 200,
"expectedRequestHeadersToUpstream": [
["$collectionHeaders"],
["X-Forwarded-For"],
["X-Real-IP"],
["Host", "example.localhost"],
["X-Forwarded-Proto", "http"],
["X-Upstream-Target", "backend:5001"]
]
},
{
"paths": ["/proxy/users"],
"method": "GET",
"expectedStatus": 200,
"expectedRequestHeadersToUpstream": [
["$collectionHeaders"],
["X-Forwarded-For"],
["X-Real-IP"],
["Host", "example.localhost"],
["X-Forwarded-Proto", "http"],
["X-Upstream-Target", "backend:5001"]
]
},
{
"paths": ["/proxy/data"],
"method": "POST",
"expectedStatus": 200,
"expectedRequestHeadersToUpstream": [
["$collectionHeaders"],
["X-Forwarded-For"],
["X-Real-IP"],
["Host", "example.localhost"],
["X-Forwarded-Proto", "http"],
["X-Upstream-Target", "backend:5001"]
]
}
]
}
}Test Coverage
The test suite validates:
- ✅ Static response status codes
- ✅ Content-Type headers (text/html, application/json)
- ✅ Proxy request forwarding
- ✅ Header transformation (X-Forwarded-For, X-Real-IP)
- ✅ Custom header propagation (collection headers)
- ✅ Host header preservation
- ✅ Protocol headers (X-Forwarded-Proto)
- ✅ Exact upstream destination verification
- ✅ Different HTTP methods (GET, POST)
Key Testing Patterns
Collection Headers
This example uses collection headers to avoid duplication:
"collectionHeaders": [
["X-Request-Id"],
["X-Path"],
["X-Verb"],
["X-QS"]
]These headers are referenced in tests using $collectionHeaders:
"expectedRequestHeadersToUpstream": [
["$collectionHeaders"],
["X-Forwarded-For"]
]This expands to all four collection headers plus X-Forwarded-For.
Upstream Header Validation
Tests verify that Nginx properly forwards headers to the backend:
"expectedRequestHeadersToUpstream": [
["X-Forwarded-For"], // Client IP forwarding
["X-Real-IP"], // Real client IP
["Host", "example.localhost"], // Host preservation
["X-Forwarded-Proto", "http"] // Protocol forwarding
]Exact Upstream Destination Validation
The Problem: How do you ensure your proxy routes to the exact correct upstream?
Consider a scenario with multiple possible upstreams:
backend:5001✅ (correct)backend:9999❌ (wrong port)different-host:5001❌ (wrong host)
All three might respond to HTTP requests, but only the correct one is the intended destination.
The Solution: HTTPTests automatically injects X-Upstream-Target headers:
- Automatic injection - HTTPTests adds this to your nginx.conf:
location /proxy/ {
proxy_pass http://backend:5001/;
proxy_set_header X-Upstream-Target "backend:5001"; # ← Auto-added
# ... other headers
}- Test validation - Your test verifies the exact destination:
{
"paths": ["/proxy/test"],
"expectedRequestHeadersToUpstream": [
["X-Upstream-Target", "backend:5001"] # ← Must match exactly
]
}- Guarantee - The test only passes if the request reaches
backend:5001specifically.
Learn more about Upstream Target Tracking.
GitHub Actions Workflow
name: Nginx Proxy Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: serviceguards-com/httptests-action@v2
with:
httptests-directory: ./nginx-proxyExpected Test Output
🔧 Adding X-Upstream-Target headers to nginx configs...
🚀 Starting environment...
🧪 Running tests for httptests-nginx-proxy
test_endpoints (__main__.IntegrationTests.test_endpoints) ... ok
→ Testing: GET example.localhost/
✓ Status code: 200 (expected 200)
✓ Response header: content-type = text/html
→ Testing: GET example.localhost/api/hello
✓ Status code: 200 (expected 200)
✓ Response header: content-type = application/json
→ Testing: GET example.localhost/api/status
✓ Status code: 200 (expected 200)
✓ Response header: content-type = application/json
→ Testing: GET example.localhost/proxy/test
✓ Status code: 200 (expected 200)
✓ Request header forwarded: x-request-id
✓ Request header forwarded: x-path
✓ Request header forwarded: x-verb
✓ Request header forwarded: x-qs
✓ Request header forwarded: x-forwarded-for
✓ Request header forwarded: x-real-ip
✓ Request header: host = example.localhost
✓ Request header: x-forwarded-proto = http
✓ Request header: x-upstream-target = backend:5001
→ Testing: POST example.localhost/proxy/data
✓ Status code: 200 (expected 200)
✓ Request header: x-upstream-target = backend:5001
============================================================
Total assertions passed: 22
============================================================Extending This Example
Add More Proxy Routes
location /api-v2/ {
proxy_pass http://backend:8080/;
proxy_set_header X-API-Version "v2";
proxy_set_header Host $host;
}Test it:
{
"paths": ["/api-v2/users"],
"method": "GET",
"expectedStatus": 200,
"expectedRequestHeadersToUpstream": [
["X-API-Version", "v2"],
["X-Upstream-Target", "backend:8080"]
]
}Add Load Balancing
# config.yml
mock:
additional_ports:
- 5001
- 5002
- 5003
network_aliases:
- backend-1
- backend-2
- backend-3upstream backend_cluster {
server backend-1:5001;
server backend-2:5002;
server backend-3:5003;
}
location /api/ {
proxy_pass http://backend_cluster/;
}Add SSL/TLS Headers
location /secure/ {
proxy_pass http://backend:5001/;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-SSL on;
}Test SSL headers:
{
"paths": ["/secure/data"],
"expectedRequestHeadersToUpstream": [
["X-Forwarded-Proto", "https"],
["X-Forwarded-SSL", "on"]
]
}Common Proxy Testing Patterns
Test Header Stripping
Verify sensitive headers are removed:
location /api/ {
proxy_pass http://backend:80/;
proxy_set_header Authorization ""; # Strip auth header
}{
"paths": ["/api/public"],
"additionalRequestHeaders": {
"Authorization": "Bearer secret"
},
"expectedRequestHeadersToUpstream": [
["Authorization", ""] // Should be empty
]
}Test Header Addition
Verify headers are added by the proxy:
location /api/ {
proxy_pass http://backend:80/;
proxy_set_header X-Proxy-Version "1.0";
proxy_set_header X-Service-ID "nginx-proxy";
}{
"paths": ["/api/users"],
"expectedRequestHeadersToUpstream": [
["X-Proxy-Version", "1.0"],
["X-Service-ID", "nginx-proxy"]
]
}Test Path Rewriting
location /old-api/ {
proxy_pass http://backend:80/new-api/;
}The mock service will show the rewritten path in its response.
Related Examples
- API Testing - REST API testing without proxying
- Microservices - Multi-service architecture
- API Gateway - Advanced gateway features
Next Steps
- Learn about Collection Headers
- Understand Upstream Target Tracking
- Explore config.yml options