Skip to content

Upstream Target Tracking

Learn how HTTPTests automatically tracks and validates exact proxy destinations, ensuring your nginx configuration routes to the correct upstream services.

The Problem

When testing proxy configurations, how do you ensure requests route to the exact correct upstream?

Consider this scenario:

yaml
# Multiple possible upstreams exist in your Docker network
mock:
  additional_ports:
    - 5001
    - 9999
  network_aliases:
    - backend
    - different-host

Your nginx config proxies to one:

nginx
location /api/ {
    proxy_pass http://backend:5001/;
}

The challenge: All these upstreams could respond to HTTP requests:

  • backend:5001 ✅ (correct)
  • backend:9999 ❌ (wrong port)
  • different-host:5001 ❌ (wrong host)

The question: How do you test that nginx proxies to backend:5001 specifically, and not any of the alternatives?

The Solution

HTTPTests automatically adds X-Upstream-Target headers to your nginx configurations, enabling precise upstream validation.

How It Works

  1. Automatic Injection: Before building containers, HTTPTests scans your nginx configs
  2. Detects proxy_pass: Finds all proxy_pass directives
  3. Adds Header: Injects proxy_set_header X-Upstream-Target "<upstream>";
  4. Preserves Format: Maintains indentation and formatting
  5. Idempotent: Safe to run multiple times

Example Transformation

Before (your original nginx.conf):

nginx
location /api/ {
    proxy_pass http://backend:5001/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

After (automatic injection):

nginx
location /api/ {
    proxy_pass http://backend:5001/;
    proxy_set_header X-Upstream-Target "backend:5001";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

The header is added immediately after proxy_pass, before any other proxy_set_header directives.

Testing Upstream Destinations

Once the header is injected, validate it in your tests:

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

This test will only pass if:

  • The request reaches the upstream service
  • The upstream is specifically backend:5001
  • Not backend:9999 or different-host:5001

Why This Matters

Scenario: Multiple Microservices

yaml
mock:
  additional_ports:
    - 5001  # User service
    - 5002  # Order service
    - 5003  # Payment service
  network_aliases:
    - user-service
    - order-service
    - payment-service
nginx
location /users/ {
    proxy_pass http://user-service:5001/;
}

location /orders/ {
    proxy_pass http://order-service:5002/;
}

location /payments/ {
    proxy_pass http://payment-service:5003/;
}

Without upstream tracking: Tests might pass even if routes are misconfigured.

With upstream tracking: Each route is validated precisely:

json
{
    "paths": ["/users/profile"],
    "expectedRequestHeadersToUpstream": [
        ["X-Upstream-Target", "user-service:5001"]
    ]
},
{
    "paths": ["/orders/12345"],
    "expectedRequestHeadersToUpstream": [
        ["X-Upstream-Target", "order-service:5002"]
    ]
},
{
    "paths": ["/payments/status"],
    "expectedRequestHeadersToUpstream": [
        ["X-Upstream-Target", "payment-service:5003"]
    ]
}

Scenario: Load Balancer Testing

nginx
upstream backend_cluster {
    server backend-1:5001;
    server backend-2:5002;
    server backend-3:5003;
}

location /api/ {
    proxy_pass http://backend_cluster/;
}

After injection:

nginx
location /api/ {
    proxy_pass http://backend_cluster/;
    proxy_set_header X-Upstream-Target "backend_cluster";
}

Test validates load balancer usage:

json
{
    "paths": ["/api/users"],
    "expectedRequestHeadersToUpstream": [
        ["X-Upstream-Target", "backend_cluster"]
    ]
}

How the Injection Works

The Script: add_upstream_headers.py

HTTPTests runs this script automatically before building containers:

bash
python add_upstream_headers.py ./your-service/

What it does:

  1. Scans all .conf files in the directory
  2. Uses regex to find proxy_pass directives
  3. Extracts the upstream URL (e.g., http://backend:5001/)
  4. Checks if X-Upstream-Target already exists
  5. If not, adds proxy_set_header X-Upstream-Target "backend:5001";
  6. Preserves indentation and formatting

Supported Patterns

The script handles various nginx patterns:

nginx
# Basic proxy_pass
proxy_pass http://backend:80/;
# → X-Upstream-Target: "backend:80"

# With path
proxy_pass http://backend:80/api/v1/;
# → X-Upstream-Target: "backend:80"

# Upstream block
proxy_pass http://backend_cluster;
# → X-Upstream-Target: "backend_cluster"

# With variables
proxy_pass http://$backend_host:$backend_port/;
# → X-Upstream-Target: "$backend_host:$backend_port"

# HTTPS
proxy_pass https://secure-backend:443/;
# → X-Upstream-Target: "secure-backend:443"

Idempotency

Running the script multiple times is safe:

bash
# First run - adds header
python add_upstream_headers.py ./service/

# Second run - skips (header exists)
python add_upstream_headers.py ./service/

# Third run - still skips
python add_upstream_headers.py ./service/

The script checks for existing X-Upstream-Target headers and skips if found.

Manual Header Addition

If you prefer manual control, you can add the header yourself:

nginx
location /api/ {
    proxy_pass http://backend:5001/;
    proxy_set_header X-Upstream-Target "backend:5001";  # Manual
    proxy_set_header Host $host;
}

The automatic script will detect this and skip injection.

Advanced Use Cases

Testing Failover

nginx
location /api/ {
    proxy_pass http://backend:5001/;
    proxy_next_upstream error timeout http_502;
}

Test that primary upstream is attempted first:

json
{
    "paths": ["/api/users"],
    "expectedRequestHeadersToUpstream": [
        ["X-Upstream-Target", "backend:5001"]
    ]
}

Testing Path Rewrites

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

Validate routing to new API path:

json
{
    "paths": ["/old-api/users"],
    "expectedRequestHeadersToUpstream": [
        ["X-Upstream-Target", "backend:80"]
    ]
}

Testing Conditional Routing

nginx
location /api/ {
    if ($http_x_version = "v2") {
        proxy_pass http://backend-v2:80/;
    }
    proxy_pass http://backend-v1:80/;
}

Test both routes:

json
{
    "paths": ["/api/users"],
    "method": "GET",
    "expectedRequestHeadersToUpstream": [
        ["X-Upstream-Target", "backend-v1:80"]
    ]
},
{
    "paths": ["/api/users"],
    "method": "GET",
    "additionalRequestHeaders": {
        "X-Version": "v2"
    },
    "expectedRequestHeadersToUpstream": [
        ["X-Upstream-Target", "backend-v2:80"]
    ]
}

Troubleshooting

Header Not Found

Issue: Test fails with "Expected header 'X-Upstream-Target' not found"

Solutions:

  1. Check automatic injection ran:
🔧 Adding X-Upstream-Target headers to nginx configs...

Should appear in logs.

  1. Verify nginx syntax:
nginx
# Correct
proxy_pass http://backend:80/;

# Incorrect (might not be detected)
proxy_pass    http://backend:80/;  # Extra spaces
  1. Check file location: Script scans .conf files in the httptests directory.

  2. Manual addition: Add the header manually if automatic injection fails.

Wrong Upstream Value

Issue: Test expects backend:5001 but receives backend:80

Solutions:

  1. Check nginx configuration:
nginx
location /api/ {
    # Make sure this matches what you expect
    proxy_pass http://backend:5001/;
}
  1. Update test:
json
{
    "expectedRequestHeadersToUpstream": [
        ["X-Upstream-Target", "backend:5001"]  // Must match nginx config
    ]
}

Best Practices

1. Always Validate Critical Routes

json
{
    "paths": ["/api/users", "/api/orders", "/api/payments"],
    "expectedRequestHeadersToUpstream": [
        ["X-Upstream-Target", "critical-service:5001"]
    ]
}

2. Test Each Microservice Route

json
// User service
{
    "paths": ["/users/profile"],
    "expectedRequestHeadersToUpstream": [
        ["X-Upstream-Target", "user-service:5001"]
    ]
},
// Order service
{
    "paths": ["/orders/12345"],
    "expectedRequestHeadersToUpstream": [
        ["X-Upstream-Target", "order-service:5002"]
    ]
}

3. Document Expected Upstreams

yaml
# config.yml
mock:
  additional_ports:
    - 5001  # User service - handles /users/*
    - 5002  # Order service - handles /orders/*
  network_aliases:
    - user-service
    - order-service

4. Validate Load Balancer Usage

json
{
    "paths": ["/api/users"],
    "expectedRequestHeadersToUpstream": [
        ["X-Upstream-Target", "user_service_cluster"]
    ]
}

Released under the MIT License.