How to Make Nginx If-Modified-Since Work with Dynamic Content (PHP/Python/JSON Responses)


14 views

Nginx's if_modified_since directive (in http_core_module) is often misunderstood when dealing with dynamic content. While it works perfectly for static files by comparing the If-Modified-Since request header with the file's modification time, dynamic responses require special handling.

# Basic static file configuration (works out-of-the-box)
location /static/ {
    if_modified_since before;
    # ...
}

The key difference: For dynamic responses, Nginx does not automatically compare the client's If-Modified-Since with your application's Last-Modified response header. You need to implement this logic either:

  • In your application code (PHP/Python), or
  • Using Nginx's proxy_cache_valid when reverse-proxying

For PHP (Laravel example):

// In your controller
$lastModified = Carbon::parse($resource->updated_at);
if ($request->header('If-Modified-Since') && 
    strtotime($request->header('If-Modified-Since')) >= $lastModified->timestamp) {
    return response()->json([], 304);
}

return response()
    ->json($data)
    ->header('Last-Modified', $lastModified->toRfc7231String());

When using PHP-FPM:

location ~ \.php$ {
    fastcgi_cache my_cache;
    fastcgi_cache_valid 200 304 1h;
    fastcgi_cache_use_stale updating error timeout invalid_header;
    fastcgi_cache_key "$scheme$request_method$host$request_uri";
    add_header X-Cache-Status $upstream_cache_status;
    
    # This makes Nginx respect your app's Last-Modified
    fastcgi_pass_header Last-Modified;
    fastcgi_pass_header Cache-Control;
    
    # Standard FastCGI config...
    include fastcgi_params;
    fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
}

Use this curl command to test if headers are properly propagating:

curl -v -H "If-Modified-Since: $(date -u -d '1 hour ago' +'%a, %d %b %Y %H:%M:%S GMT')" \
http://yoursite.com/api/endpoint

Check for these critical headers in the response:

  • Last-Modified (must match your app's timestamp format)
  • Cache-Control (shouldn't have no-cache or private for this use case)
  • Status code (should be 304 when content hasn't changed)

When implementing Last-Modified headers for dynamic content in Nginx, many developers notice unexpected behavior with the if_modified_since directive. The key observation is:

# Typical response from dynamic backend (PHP/Python)
HTTP/1.1 200 OK
Last-Modified: Fri, 01 May 2015 19:56:05 GMT
Content-Type: application/json

# Despite If-Modified-Since header from client:
GET /api/data HTTP/1.1
If-Modified-Since: Fri, 01 May 2015 18:00:00 GMT

Nginx's handling differs between static and dynamic content:

  • Static files: Nginx compares modification timestamps directly from filesystem
  • Dynamic content: The backend must explicitly handle 304 responses

For dynamic content, you'll need additional configuration:

location ~ \.php$ {
    # Ensure PHP application receives the If-Modified-Since header
    fastcgi_param HTTP_IF_MODIFIED_SINCE $http_if_modified_since;
    # Your regular FastCGI params...
}

Here's a full implementation example for PHP applications:

// In your PHP application
$lastModified = // Your logic to determine last modified time
header("Last-Modified: " . gmdate("D, d M Y H:i:s", $lastModified) . " GMT");

if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
    $ifModifiedSince = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
    if ($ifModifiedSince >= $lastModified) {
        header('HTTP/1.1 304 Not Modified');
        exit;
    }
}

Use this curl command to verify your implementation:

curl -I -H "If-Modified-Since: Fri, 01 May 2015 18:00:00 GMT" http://yoursite.com/api

Expected responses:

  • 200 OK when content is newer
  • 304 Not Modified when content hasn't changed

For more complex scenarios:

# Nginx configuration to cache dynamic responses
proxy_cache_valid 200 302 304 5m;
proxy_cache_key "$scheme$request_method$host$request_uri$http_if_modified_since";

Remember that proper ETag implementation often works better with dynamic content.