Nginx HTTPS Redirect Converting POST to GET: Permanent Redirect Issue and Solution


2 views

Recently while configuring an Nginx reverse proxy setup, I encountered an interesting issue where HTTP POST requests were being converted to GET requests after an HTTPS redirect. Here's the exact architecture:

Client → Public IP (A) with HTTPS → Internal VM (B) with HTTP

The problematic part in the Nginx config was:

server {
    listen 80;
    rewrite ^(.*) https://$http_host$1 permanent;
    # ... other configs ...
}

This seemingly innocent rewrite was causing all POST requests to transform into GET requests when redirected to HTTPS.

The root cause lies in how HTTP 301/302 redirects work:

  • Browser behavior: When receiving a 301/302 response to a POST request, most clients will convert it to GET (as per HTTP spec)
  • The permanent flag in rewrite generates a 301 status
  • This is standard HTTP behavior, not an Nginx bug

We have several options to handle this:

Option 1: Use 307/308 Temporary Redirect

307/308 status codes preserve the request method:

server {
    listen 80;
    return 307 https://$http_host$request_uri;
}

Option 2: Handle SSL Termination Differently

Instead of redirecting, proxy directly:

server {
    listen 80;
    location / {
        proxy_pass https://backend;
        # ... proxy settings ...
    }
}

Option 3: Client-side Handling

For API clients, you can make them:

# Python example with direct HTTPS
import requests
url = 'https://IP_A/api/service/signup'  # Note HTTPS
res = requests.post(url, data=data, verify=False)

Here's what I recommend for most production environments:

# HTTP server block
server {
    listen 80;
    server_name example.com;
    
    # For browsers - redirect GET/HEAD requests
    if ($request_method ~ ^(GET|HEAD)$) {
        return 301 https://$host$request_uri;
    }
    
    # For API clients - proxy with error message
    location / {
        add_header Content-Type "application/json";
        return 405 '{"error": "Please use HTTPS directly"}';
    }
}

Verify with curl:

# Should preserve POST
curl -X POST http://example.com/api -L -v

# Should see the original POST method preserved

Remember to test with your actual client implementations as behavior may vary between HTTP libraries.

  • Update all API documentation to use HTTPS endpoints
  • Consider implementing HSTS header for browsers
  • Monitor for any legacy clients that might still try HTTP POST

During a recent deployment, we noticed an unexpected behavior in our Nginx proxy setup where HTTP POST requests were being converted to GET requests after the HTTPS rewrite. Here's the symptom we observed:

# Python test script showing the issue
import requests

data = {'username': 'test', 'password': 'test123'}
url = 'http://PROXY_IP/api/service/signup'

res = requests.post(url, data=data, verify=False)
# Returns 405 Method Not Allowed

The issue stems from using a permanent redirect (HTTP 301) in the rewrite rule. According to HTTP/1.1 specifications (RFC 2616), browsers and clients should convert POST requests to GET when following 301/302 redirects.

# Problematic rewrite rule in Nginx config
server {
    listen 80;
    rewrite ^(.*) https://$http_host$1 permanent;
    # ...
}

Instead of using a permanent redirect, we should use HTTP 307 (Temporary Redirect) which preserves the request method. Here's the corrected configuration:

server {
    listen 80;
    return 307 https://$http_host$request_uri;
    
    server_name localhost 127.0.0.1;
    server_name_in_redirect off;
    
    # Other proxy settings...
}

If you need to maintain backward compatibility with older clients that don't support HTTP 307, consider these options:

# Option 1: Conditional rewrite based on request method
server {
    listen 80;
    
    if ($request_method = POST) {
        return 307 https://$host$request_uri;
    }
    
    return 301 https://$host$request_uri;
}

# Option 2: Proxy passing directly (no redirect)
server {
    listen 80;
    location / {
        proxy_pass https://backend_server;
        # Other proxy settings...
    }
}

After implementing the solution, verify it with both command line tools and browser requests:

# Using cURL to test
curl -X POST -d "test=data" http://yourdomain.com/api
# Should maintain POST method through redirect

# Python verification
import requests
r = requests.post('http://yourdomain.com/api', data={'key':'value'})
print(r.status_code)  # Should return 200, not 405

While 307 redirects solve the method preservation issue, they have some implications:

  • 307 responses aren't cached by default by most clients
  • Each request will incur an additional roundtrip
  • For high-traffic sites, consider handling SSL termination at the proxy level

When working with proxy setups, these headers are crucial for proper request handling:

proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;