Nginx client_max_body_size Configuration for PHP Front Controller Pattern with Location-Based Upload Limits


2 views

When implementing upload size restrictions in Nginx with PHP front controller patterns, we often encounter this scenario:

location /admin/upload {
    client_max_body_size 256M;
}

location ~ \.php$ {
    # PHP processing configuration
    fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
    # Other FastCGI params...
}

The upload size limit in /admin/upload never triggers because:

  • All PHP requests ultimately match the ~ \.php$ location
  • Nginx processes only one location block per request
  • The PHP handler location lacks the upload size directive

Here are three proven approaches:

1. Early Phase Checking

server {
    client_max_body_size 512K;

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ^~ /admin/upload {
        client_max_body_size 256M;
        try_files $uri /index.php$is_args$args;
    }

    location ~ \.php$ {
        # Check the original request URI
        if ($request_uri ~* "^/admin/upload") {
            client_max_body_size 256M;
        }
        
        # Standard PHP handler configuration
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        include fastcgi_params;
    }
}

2. Separate Upload Endpoint

Create a dedicated upload handler:

location /admin/upload {
    client_max_body_size 256M;
    fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
    include fastcgi_params;
    
    # Custom FastCGI parameters for uploads
    fastcgi_param SCRIPT_FILENAME $document_root/upload_handler.php;
}

location ~ \.php$ {
    client_max_body_size 512K;
    # Standard PHP configuration...
}

3. Map-Based Configuration

For more complex scenarios:

map $request_uri $upload_limit {
    ~^/admin/upload 256M;
    default 512K;
}

server {
    client_max_body_size $upload_limit;
    
    # Standard configuration...
}

When implementing upload restrictions:

  • Always validate file types in your PHP application
  • Consider using temporary storage for large uploads
  • Implement CSRF protection for upload forms
  • Set appropriate timeout values: client_body_timeout and client_header_timeout

For high-traffic sites:

location /admin/upload {
    client_max_body_size 256M;
    client_body_buffer_size 128k;
    client_body_temp_path /var/nginx/client_body_temp 1 2;
    proxy_request_buffering off;
    
    # Rest of the configuration...
}

When using Nginx with PHP front controller pattern (like Laravel, Symfony, etc.), we often face a challenge: we want to allow large file uploads only for specific routes (like /admin/upload), while keeping the default upload size small for security. The standard approach of setting client_max_body_size in location blocks doesn't work as expected because PHP requests are ultimately handled by the PHP location block.

The issue occurs because Nginx processes locations in a specific order, and the PHP handler location (~ \.php$) typically catches all PHP requests, overriding any client_max_body_size set in other locations. Here's what happens:


# This won't work as expected:
location /admin/upload {
    client_max_body_size 256M;
}

location ~ \.php$ {
    # All PHP requests end up here, ignoring the upload limit
    fastcgi_pass unix:/tmp/php5-fpm.sock;
}

We need to combine two techniques:


server {
    # Default upload limit
    client_max_body_size 512K;

    location / {
        try_files $uri /index.php?$query_string;
    }

    # Special upload location
    location ^~ /admin/upload {
        client_max_body_size 256M;
        
        # Pass to PHP handler while preserving the limit
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root/index.php;
        fastcgi_pass unix:/tmp/php5-fpm.sock;
    }

    # Regular PHP handler
    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/tmp/php5-fpm.sock;
    }
}

The magic happens with:

  • ^~ prefix: Makes this location take precedence over regex locations
  • Explicit PHP handling: We manually pass the request to PHP while maintaining our custom limit
  • Preserved SCRIPT_FILENAME: Ensures the front controller still receives the request

Verify with curl:


# Should fail (512K limit)
curl -X POST -H "Content-Type: multipart/form-data" \
  -F "file=@largefile.zip" http://example.com/upload

# Should succeed (256M limit)
curl -X POST -H "Content-Type: multipart/form-data" \
  -F "file=@largefile.zip" http://example.com/admin/upload

For Laravel/Symfony, you might need additional routing configuration to ensure the upload endpoint is properly handled by your application while respecting Nginx's size limits.