Solving URL Rewriting Issues When Using ProxyPass with Django and Apache


2 views

When setting up reverse proxy configurations with Apache's mod_proxy, many developers encounter URL path issues when applications perform redirects. The core issue manifests when:

  • Your main application (e.g., PHP) runs on Machine #1
  • A secondary application (e.g., Django) runs on Machine #2
  • You configure ProxyPass /new http://192.168.0.101/
  • The Django app generates redirects like /auth/login instead of maintaining the /new prefix

Django's URL resolution works independently of your proxy configuration. When it generates redirects:

# Django's view.py example
def login_redirect(request):
    return HttpResponseRedirect('/auth/login/')  # This breaks proxy paths

The redirect doesn't account for your /new prefix because Django only sees the request path after ProxyPass processing.

To properly handle this, we need both ProxyPass and ProxyPassReverse:

<VirtualHost *:80>
    ServerName foo.net
    
    # Main PHP app
    DocumentRoot /var/www/php_app
    
    # Django proxy configuration
    ProxyPass /new http://192.168.0.101/
    ProxyPassReverse /new http://192.168.0.101/
    
    # Additional headers to help with URL generation
    ProxyPreserveHost On
    RequestHeader set X-Forwarded-Proto "http"
    RequestHeader set X-Forwarded-Prefix "/new"
</VirtualHost>

Configure Django to recognize proxy headers:

# settings.py
USE_X_FORWARDED_HOST = True
FORCE_SCRIPT_NAME = '/new'  # Only if you want all URLs prefixed

# Alternative middleware approach
MIDDLEWARE += [
    'django.middleware.common.BrokenLinkEmailsMiddleware',
    'django.middleware.common.CommonMiddleware',
]

When using URL reversing in Django templates:

{% raw %}
# Good (works with proxy)
<a href="{% url 'login' %}">Login</a>

# Bad (hardcoded paths break with proxy)
<a href="/auth/login/">Login</a>
{% endraw %}

Verify with curl commands:

# Test basic proxy functionality
curl -I http://foo.net/new/

# Check redirect headers
curl -I http://foo.net/new/auth/login/

# Verify X-Forwarded headers
curl -H "X-Forwarded-Proto: https" -I http://foo.net/new/

For more control, create custom middleware:

# middleware.py
class URLPrefixMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        request.path_info = '/new' + request.path_info
        return self.get_response(request)

Add to MIDDLEWARE after CommonMiddleware.


When configuring Apache's mod_proxy to route requests to a Django application, the default ProxyPass behavior often breaks Django's URL routing. The issue manifests when Django generates absolute URLs without accounting for the proxy path prefix (/new in your case).

Here's the minimal working configuration that preserves URL structure:

<VirtualHost *:80>
    ServerName foo.net

    ProxyPass /new http://192.168.0.101/
    ProxyPassReverse /new http://192.168.0.101/

    # Critical for Django static files
    Alias /new/static /path/to/django/static
    <Directory /path/to/django/static>
        Require all granted
    </Directory>
</VirtualHost>

Your settings.py needs these modifications:

# Required for proxy setups
USE_X_FORWARDED_HOST = True
FORCE_SCRIPT_NAME = '/new'

# Important for URL generation
STATIC_URL = FORCE_SCRIPT_NAME + '/static/'
MEDIA_URL = FORCE_SCRIPT_NAME + '/media/'

For complex cases, implement middleware to handle URL rewriting:

class ProxyPathMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        
    def __call__(self, request):
        request.META['SCRIPT_NAME'] = '/new'
        response = self.get_response(request)
        
        if 'Location' in response:
            response['Location'] = '/new' + response['Location']
        return response

Verify your setup with these curl commands:

curl -I http://foo.net/new/admin/
curl -v http://foo.net/new/static/css/styles.css

Look for proper 200 responses and correct Content-Type headers.

  • Forgetting trailing slashes in ProxyPass directives
  • Not setting FORCE_SCRIPT_NAME in Django
  • Missing ProxyPassReverse for redirect handling
  • Incorrect static file paths in both Apache and Django