How to Properly Handle Relative URLs in Nginx Reverse Proxy with Subdirectory Routing


2 views

When setting up a reverse proxy where you need to serve content from example.com under example.net/bbb/, several URL handling challenges emerge:

# Current problematic config snippet
location /bbb/ {
    proxy_pass http://example.com/;
    proxy_set_header Host $host;
    # Missing critical headers for proper URL resolution
}

To properly handle both HTML content and static assets, we need multiple adjustments:

location /bbb/ {
    proxy_pass http://example.com/;
    proxy_set_header Host example.com;  # Critical change
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    
    # Rewrite response headers containing URLs
    proxy_redirect ~^/(.*)$ /bbb/$1;
    
    # Handle redirects from backend
    proxy_intercept_errors on;
    recursive_error_pages on;
    error_page 301 302 307 = @handle_redirects;
}

location @handle_redirects {
    set $saved_redirect_location '$upstream_http_location';
    if ($saved_redirect_location ~ ^/(.*)$) {
        return 301 /bbb/$1;
    }
    proxy_pass $saved_redirect_location;
}

For complete URL resolution, modify the served HTML (if possible):

<!-- In your application's head section -->
<base href="/bbb/" />

When you can't modify backend HTML, use Nginx sub_filter:

location /bbb/ {
    # ... previous proxy settings ...
    sub_filter 'src="/' 'src="/bbb/';
    sub_filter 'href="/' 'href="/bbb/';
    sub_filter_types text/html text/css application/javascript;
    sub_filter_once off;
}

Here's a complete solution combining all techniques:

server {
    listen 80;
    server_name example.net;
    
    location /bbb/ {
        proxy_pass http://example.com/;
        proxy_set_header Host example.com;
        proxy_set_header X-Real-IP $remote_addr;
        
        # URL rewriting in responses
        proxy_redirect ~^/(.*)$ /bbb/$1;
        
        # Content rewriting
        sub_filter '="/' '="/bbb/';
        sub_filter '=/' '=/bbb/';
        sub_filter_types *;
        sub_filter_once off;
        
        # Redirect handling
        proxy_intercept_errors on;
        recursive_error_pages on;
        error_page 301 302 307 = @handle_redirect;
    }
    
    location @handle_redirect {
        set $saved_location '$upstream_http_location';
        if ($saved_location ~ ^/(.*)$) {
            return 301 /bbb/$1;
        }
        proxy_pass $saved_location;
    }
}

Verify with these curl commands:

# Test HTML content
curl -I http://example.net/bbb/

# Test asset loading
curl -I http://example.net/bbb/css/main.css

# Test redirect handling
curl -v http://example.net/bbb/old-path

When configuring Nginx to reverse proxy from a subdirectory (like /bbb) to a different domain's root, several URL resolution issues commonly occur:

location /bbb/ {
    proxy_pass http://example.com/;
    # Standard headers aren't enough
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

The primary issues stem from how browsers and servers interpret relative URLs differently:

  • Relative paths in HTML/CSS/JS resolve against the browser's current URL
  • Proxy servers don't automatically rewrite these paths
  • The trailing slash in proxy_pass affects path processing

Here's the complete working configuration with all necessary components:

server {
    listen 80;
    server_name example.net;
    root /path/to/aaa;

    location /bbb/ {
        proxy_pass http://example.com/;  # Critical trailing slash
        proxy_set_header Host example.com;  # Original host, not $host
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # URL rewriting for HTML content
        sub_filter_once off;
        sub_filter 'href="/' 'href="/bbb/';
        sub_filter 'src="/' 'src="/bbb/';
        sub_filter 'url(/' 'url(/bbb/';
        
        # Handle redirects from backend
        proxy_redirect http://example.com/ /bbb/;
        proxy_redirect / /bbb/;
    }

    # Static assets handling
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|ttf|woff|woff2)$ {
        expires max;
        access_log off;
    }
}

1. Proper Host Header

Set the original host (not the proxy host) to maintain application functionality:

proxy_set_header Host example.com;

2. HTML Content Rewriting

The sub_filter directives handle path rewriting in HTML, CSS, and JavaScript:

sub_filter 'href="/' 'href="/bbb/';
sub_filter 'src="/' 'src="/bbb/';
sub_filter 'url(/' 'url(/bbb/';

3. Redirect Handling

Backend redirects need to be rewritten to maintain the /bbb prefix:

proxy_redirect http://example.com/ /bbb/;
proxy_redirect / /bbb/;

For SPAs (Single Page Applications), you'll need additional configuration:

location /bbb/ {
    # Existing proxy config...
    
    # Handle HTML5 mode routing
    error_page 404 =200 /bbb/index.html;
    
    # API proxy exceptions
    location /bbb/api/ {
        proxy_pass http://example.com/api/;
        proxy_set_header Host example.com;
    }
}

Verify your configuration with these curl commands:

# Check HTML rewriting
curl -v http://example.net/bbb/

# Verify asset paths
curl -v http://example.net/bbb/main.css

# Test API routing
curl -v http://example.net/bbb/api/status

Remember to test with different types of relative URLs including:

  • Root-relative (/assets/image.png)
  • Protocol-relative (//example.com/style.css)
  • Path-relative (../parent/file.js)