How to Fix Nginx Reverse Proxy Returning 301 Redirect for GraphQL API Endpoints


2 views

When setting up Nginx as a reverse proxy for containerized applications, a common issue occurs where POST requests to API endpoints like /graphql get unexpectedly converted to 301 redirects. This breaks client-side applications making AJAX requests, particularly in modern JavaScript frameworks.

The 301 redirect occurs because of Nginx's default behavior with URI processing. When Nginx sees /graphql without a trailing slash, it assumes this should be a directory and automatically redirects to /graphql/. This behavior makes sense for static content but causes problems with API endpoints.

Here's the corrected Nginx configuration that solves this issue:

upstream graphql-upstream {
    least_conn;
    server graphql:3000;
    keepalive 64;
}

server {
    listen 80;
    server_name domain.com www.domain.com;
    
    location / {
        proxy_pass http://frontend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
    }

    location /graphql {
        proxy_pass http://graphql-upstream;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # Important: Disable redirect behavior
        proxy_redirect off;
        
        # CORS headers
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    }
}

1. proxy_redirect off: This directive prevents Nginx from modifying the Location header in 3xx responses

2. Simplified proxy_pass: Using just the upstream name without the path segment

3. Consistent Host headers: Ensuring headers are properly passed through

After applying these changes, test with curl:

curl -X POST \
  http://domain.com/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query":"{ hello }"}'

You should now receive a direct response from your GraphQL service instead of a redirect.

For production environments, consider adding:

  • SSL termination
  • Request rate limiting
  • Proper CORS configuration
  • Health checks for the upstream services

When setting up Nginx as a reverse proxy for containerized Node.js applications, a common issue arises where POST requests to endpoints like /graphql get unexpectedly redirected with 301 status codes. This behavior breaks API clients expecting direct responses.

# Bad behavior example:
POST http://domain.com/graphql → 301 → http://domain.com/graphql/

The issue occurs because Nginx automatically adds trailing slashes to locations when:

  • The proxy_pass directive contains a URI path component
  • No exact match exists for the requested URL
  • The location block doesn't explicitly handle trailing slash behavior

Here's the corrected configuration that prevents unwanted redirects:

location /graphql {
    # Remove the path from proxy_pass target
    proxy_pass http://graphql-upstream;
    
    # Required headers for GraphQL
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    
    # CORS headers
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
    
    # Connection upgrades
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_cache_bypass $http_upgrade;
}

# Alternative exact match version
location = /graphql {
    proxy_pass http://graphql-upstream;
    # ... same headers as above
}

The working solution makes these critical changes:

  1. Removes the /graphql path from the upstream target
  2. Uses either standard location or exact match (=) syntax
  3. Maintains all necessary proxy headers and CORS support

Verify with curl commands:

# Test POST request (should return 200, not 301)
curl -X POST http://domain.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ test }"}'

For production environments, consider adding:

# Rate limiting
limit_req_zone $binary_remote_addr zone=graphql:10m rate=10r/s;

# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;