How to Conditionally Set Expires Headers Based on MIME Type in Nginx


3 views

When working with dynamically generated assets (CSS, JS, images) in Nginx, simply checking file extensions won't work for setting cache headers. The standard approach of matching file patterns fails when PHP generates these resources on-the-fly.

Your initial attempt used:

if ($sent_http_content_type = "text/css") {
    expires 7d;
}

This doesn't work because $sent_http_content_type isn't available during the rewrite phase when expires directives are processed. The header isn't set yet when Nginx evaluates this condition.

Here's the proper way to handle MIME-type-based caching in Nginx 1.4.1:

http {
    map $sent_http_content_type $expires {
        default                    off;
        "text/css"                 7d;
        "application/javascript"   7d;
        "image/jpeg"               30d;
        "image/png"                30d;
        "image/webp"               30d;
        "font/woff2"               90d;
    }

    server {
        location / {
            expires $expires;
            # Your other config...
        }
    }
}

For PHP-generated content, add these headers in your PHP script first:

header('Content-Type: text/css'); // or appropriate MIME type
header('X-Accel-Expires: 604800'); // 7 days in seconds

Then in Nginx:

location ~ \.php$ {
    fastcgi_ignore_headers X-Accel-Expires;
    expires $expires;
    # Your PHP-FPM config...
}

After implementing, verify with curl:

curl -I http://yoursite.com/path/to/asset.css

Look for the Expires and Cache-Control headers in the response.

The map directive is evaluated at runtime but highly optimized in Nginx. For high-traffic sites, consider:

  • Using static file patterns where possible
  • Implementing a CDN for dynamic assets
  • Adding cache-control: public for shared proxies

When dealing with dynamically generated assets (CSS, JS, images) in PHP applications, traditional file extension-based caching approaches fall short. The server needs to examine the actual Content-Type header before deciding cache duration.

The common pitfall is trying to use response headers in rewrite/if conditions. Nginx processes these during request phase, before response headers exist. Here's why your approach didn't work:

# This WON'T work as expected
if ($sent_http_content_type = "text/css") {
    expires 7d;
}

Solution 1: Map Block with Known Extensions

For cases where you can map file patterns to MIME types:

map $request_uri $expires {
    default off;
    ~*\.css$ 7d;
    ~*\.js$ 7d;
    ~*\.(jpg|jpeg|gif|png|webp)$ 30d;
}

server {
    expires $expires;
    # other config...
}

Solution 2: Lua Module for Dynamic Inspection

For true MIME-type detection, use ngx_http_lua_module:

location / {
    header_filter_by_lua_block {
        if ngx.header.content_type == "text/css" then
            ngx.header["Expires"] = "7d"
        end
    }
    # other config...
}

Solution 3: Proxy Cache with Header Inspection

For reverse proxy scenarios:

location / {
    proxy_pass http://backend;
    proxy_ignore_headers Set-Cookie;
    
    proxy_cache_valid 200 302 10m;
    proxy_cache_valid 301 1h;
    proxy_cache_valid any 1m;
    
    proxy_cache_bypass $http_pragma;
    proxy_cache_revalidate on;
    
    # Set expires based on backend response
    add_header X-Cache-Status $upstream_cache_status;
    expires $upstream_http_x_expires;
}

Remember that content-type based expiration requires:

  • Additional processing overhead
  • Potential race conditions with upstream responses
  • Careful testing with vary headers

Verify with curl:

curl -I http://yoursite.com/style.css | grep -i "expires\|content-type"

Or using nginx -T to test configuration syntax before reloading.