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@v2
        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.