Handling HTTP Redirects in Nginx Proxy: How to Follow and Consolidate Redirect Chains


2 views

When building an Nginx proxy server, one common requirement is to handle HTTP redirects (301, 302, 307) internally so clients only receive the final response. The default behavior shows all redirect steps to clients, which might not be ideal for certain proxy implementations.

The core issue lies in Nginx's default handling of upstream redirects. When a backend server returns a 3xx status with a Location header, Nginx simply passes this response to the client. For a proxy that should consolidate the entire redirect chain, we need to:

  • Intercept 3xx responses
  • Extract the Location header
  • Follow the redirect internally
  • Only return the final response

Here's a complete implementation that handles redirects internally:

server {
    listen 80;
    
    location /proxy/ {
        # Remove /proxy/ prefix
        rewrite ^/proxy/(.*)$ /$1 break;
        
        # Store the original request URI
        set $original_uri $uri;
        
        # Initial proxy pass
        proxy_pass http://backend_server$uri$is_args$args;
        
        # Handle redirects
        proxy_intercept_errors on;
        error_page 301 302 303 307 = @handle_redirect;
    }
    
    location @handle_redirect {
        # Get the Location header from the response
        set $redirect_location $upstream_http_location;
        
        # Validate the redirect location
        if ($redirect_location ~* ^https?://[^/]+(/.*)$) {
            set $redirect_location $1;
        }
        
        # Follow the redirect
        proxy_pass http://backend_server$redirect_location;
    }
}

proxy_intercept_errors: Enables interception of error responses (including redirects) from upstream servers.

error_page: Redirects specific HTTP status codes to a named location block.

$upstream_http_location: Contains the Location header from the upstream response.

For handling multiple redirects in a chain, we can implement a recursive solution using the ngx_http_lua_module:

location /smartproxy/ {
    rewrite ^/smartproxy/(.*)$ /$1 break;
    
    access_by_lua_block {
        local http = require "resty.http"
        local httpc = http.new()
        
        local res, err = httpc:request_uri("http://backend_server" .. ngx.var.uri, {
            method = ngx.var.request_method,
            headers = ngx.req.get_headers(),
            query = ngx.var.args,
            redirect = 5  -- Maximum number of redirects to follow
        })
        
        if not res then
            ngx.log(ngx.ERR, "request failed: ", err)
            return ngx.exit(500)
        end
        
        ngx.status = res.status
        for k, v in pairs(res.headers) do
            if k ~= "transfer-encoding" then
                ngx.header[k] = v
            end
        end
        
        ngx.print(res.body)
        return ngx.exit(res.status)
    }
}

When implementing redirect following in Nginx:

  • Set reasonable timeout values (proxy_connect_timeout, proxy_read_timeout)
  • Limit the maximum number of redirects to prevent infinite loops
  • Consider caching responses for frequently accessed URLs
  • Monitor upstream server performance to avoid bottlenecks

Verify your redirect handling with curl:

curl -v http://your_proxy_server/proxy/redirecting_url

You should only see the final response, not intermediate redirects.


When working with nginx as a reverse proxy, one common issue developers face is properly handling HTTP redirect responses (301, 302, 307) from upstream servers. The default behavior passes these redirects directly to clients, which may not be the desired outcome.

The core issue lies in nginx's default proxy behavior where:

  • Redirect responses from upstream servers are passed through to clients
  • The proxy doesn't automatically follow redirect chains
  • Header information isn't readily available for processing

Here's an effective approach to handle redirects internally:

server {
    # ... other server config ...

    location /proxy {
        rewrite ^/proxy/([^/]+) $1 break;
        proxy_pass http://$uri/;
        
        # Capture redirect headers
        proxy_set_header Host $host;
        proxy_redirect off;
        
        # Intercept redirect responses
        error_page 301 302 307 = @handle_redirect;
    }

    location @handle_redirect {
        # Extract Location header from previous response
        set $redirect_location $upstream_http_location;
        
        # Validate and sanitize the redirect URL
        if ($redirect_location ~* ^https?://[^/]+(/.*)?$) {
            proxy_pass $redirect_location;
        }
        
        # Fallback if redirect URL is invalid
        return 500 "Invalid redirect location";
    }
}

For handling multiple redirect levels and complex scenarios:

map $upstream_http_location $redirect_target {
    ~^(https?://[^/]+)(/.*)?$ $1;
    default "";
}

server {
    # ... server config ...

    location /proxy {
        proxy_pass http://backend/;
        proxy_intercept_errors on;
        error_page 301 302 307 = @follow_redirect;
    }

    location @follow_redirect {
        # Prevent redirect loops
        if ($request_uri ~ "^/proxy") {
            return 500 "Redirect loop detected";
        }
        
        # Follow the redirect
        proxy_pass $redirect_target;
        proxy_set_header Host $host;
        proxy_redirect off;
    }
}
  • Always validate redirect URLs to prevent open redirect vulnerabilities
  • Consider setting a maximum redirect limit to avoid infinite loops
  • Handle HTTPS redirects properly by maintaining protocol consistency
  • Preserve original request headers where needed

While this solution works, be aware that:

  • Each redirect adds latency to the request
  • Multiple redirects may impact proxy performance
  • DNS resolution occurs for each redirect target
  • Consider caching resolved endpoints when possible