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