Skip to content

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.json

Configuration Files ​

Dockerfile ​

dockerfile
FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

nginx.conf ​

nginx
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/hello and /api/status return JSON directly
  • /proxy/* forwards to backend:5001 with header transformation
  • Standard proxy headers: X-Real-IP, X-Forwarded-For, X-Forwarded-Proto
  • Host header preservation

.httptests/config.yml ​

yaml
mock:
  additional_ports:
    - 5001
  network_aliases:
    - backend

nginx:
  environment:
    PROXY_VERSION: v1

Configuration breakdown:

  • additional_ports: [5001] - Mock service accessible on port 5001
  • network_aliases: [backend] - Service reachable as backend in Docker network
  • Environment variable for the Nginx container

.httptests/test.json ​

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:

json
"collectionHeaders": [
    ["X-Request-Id"],
    ["X-Path"],
    ["X-Verb"],
    ["X-QS"]
]

These headers are referenced in tests using $collectionHeaders:

json
"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:

json
"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:

  1. Automatic injection - HTTPTests adds this to your nginx.conf:
nginx
location /proxy/ {
    proxy_pass http://backend:5001/;
    proxy_set_header X-Upstream-Target "backend:5001";  # ← Auto-added
    # ... other headers
}
  1. Test validation - Your test verifies the exact destination:
json
{
    "paths": ["/proxy/test"],
    "expectedRequestHeadersToUpstream": [
        ["X-Upstream-Target", "backend:5001"]  # ← Must match exactly
    ]
}
  1. Guarantee - The test only passes if the request reaches backend:5001 specifically.

Learn more about Upstream Target Tracking.

GitHub Actions Workflow ​

yaml
name: Nginx Proxy Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: serviceguards-com/httptests-action@latest
        with:
          httptests-directory: ./nginx-proxy

Expected 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 ​

nginx
location /api-v2/ {
    proxy_pass http://backend:8080/;
    proxy_set_header X-API-Version "v2";
    proxy_set_header Host $host;
}

Test it:

json
{
    "paths": ["/api-v2/users"],
    "method": "GET",
    "expectedStatus": 200,
    "expectedRequestHeadersToUpstream": [
        ["X-API-Version", "v2"],
        ["X-Upstream-Target", "backend:8080"]
    ]
}

Add Load Balancing ​

yaml
# config.yml
mock:
  additional_ports:
    - 5001
    - 5002
    - 5003
  network_aliases:
    - backend-1
    - backend-2
    - backend-3
nginx
upstream 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 ​

nginx
location /secure/ {
    proxy_pass http://backend:5001/;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-SSL on;
}

Test SSL headers:

json
{
    "paths": ["/secure/data"],
    "expectedRequestHeadersToUpstream": [
        ["X-Forwarded-Proto", "https"],
        ["X-Forwarded-SSL", "on"]
    ]
}

Common Proxy Testing Patterns ​

Test Header Stripping ​

Verify sensitive headers are removed:

nginx
location /api/ {
    proxy_pass http://backend:80/;
    proxy_set_header Authorization "";  # Strip auth header
}
json
{
    "paths": ["/api/public"],
    "additionalRequestHeaders": {
        "Authorization": "Bearer secret"
    },
    "expectedRequestHeadersToUpstream": [
        ["Authorization", ""]  // Should be empty
    ]
}

Test Header Addition ​

Verify headers are added by the proxy:

nginx
location /api/ {
    proxy_pass http://backend:80/;
    proxy_set_header X-Proxy-Version "1.0";
    proxy_set_header X-Service-ID "nginx-proxy";
}
json
{
    "paths": ["/api/users"],
    "expectedRequestHeadersToUpstream": [
        ["X-Proxy-Version", "1.0"],
        ["X-Service-ID", "nginx-proxy"]
    ]
}

Test Path Rewriting ​

nginx
location /old-api/ {
    proxy_pass http://backend:80/new-api/;
}

The mock service will show the rewritten path in its response.

Next Steps ​

Released under the MIT License.