Home > Software engineering >  Header expiration in htaccess not working in sub-folder
Header expiration in htaccess not working in sub-folder

Time:10-18

In a .htaccess the expiration headers are set (let's call it the primary htaccess file). Then in a specific folder of this I have another .htaccess file because I would like to have an exception and have different expiration headers for files in that subfolder. This subfolder contains test.min.css.

However, when testing it, the file test.min.css still has the expiration header of max-age=9072000, I assume coming from the .htaccess file a folder up.

What am I doing wrong in the .htaccess file in the subfolder?

The primary .htaccess file is as follows (it's a Wordpress site):

#Expires headers configuration added by BREEZE WP CACHE plugin
<IfModule mod_env.c>
   SetEnv BREEZE_BROWSER_CACHE_ON 1
</IfModule>
<IfModule mod_expires.c>
   ExpiresActive On
   ExpiresDefault "access plus 1 month"
   # Assets
   ExpiresByType text/css "access plus 1 month"
   ExpiresByType application/javascript "access plus 1 month"
   ExpiresByType application/x-javascript "access plus 1 month"
   ExpiresByType text/javascript "access plus 1 month"
   # Media assets 
   ExpiresByType audio/ogg "access plus 1 year"
   ExpiresByType image/bmp "access plus 1 year"
   ExpiresByType image/gif "access plus 1 year"
   ExpiresByType image/jpeg "access plus 1 year"
   ExpiresByType image/png "access plus 1 year"
   ExpiresByType image/svg xml "access plus 1 year"
   ExpiresByType image/webp "access plus 1 year"
   ExpiresByType video/mp4 "access plus 1 year"
   ExpiresByType video/ogg "access plus 1 year"
   ExpiresByType video/webm "access plus 1 year"
   # Font assets 
   ExpiresByType application/vnd.ms-fontobject "access plus 1 year"
   ExpiresByType font/eot "access plus 1 year"
   ExpiresByType font/opentype "access plus 1 year"
   ExpiresByType application/x-font-ttf "access plus 1 year"
   ExpiresByType application/font-woff "access plus 1 year"
   ExpiresByType application/x-font-woff "access plus 1 year"
   ExpiresByType font/woff "access plus 1 year"
   ExpiresByType application/font-woff2 "access plus 1 year"
   # Data interchange
   ExpiresByType application/xml "access plus 0 seconds"
   ExpiresByType application/json "access plus 0 seconds"
   ExpiresByType application/ld json "access plus 0 seconds"
   ExpiresByType application/schema json "access plus 0 seconds"
   ExpiresByType application/vnd.geo json "access plus 0 seconds"
   ExpiresByType text/xml "access plus 0 seconds"
   ExpiresByType application/rss xml "access plus 1 hour"
   ExpiresByType application/rdf xml "access plus 1 hour"
   ExpiresByType application/atom xml "access plus 1 hour"
   # Manifest files
   ExpiresByType application/manifest json "access plus 1 week"
   ExpiresByType application/x-web-app-manifest json "access plus 0 seconds"
   ExpiresByType text/cache-manifest  "access plus 0 seconds"
   # Favicon
   ExpiresByType image/vnd.microsoft.icon "access plus 1 week"
   ExpiresByType image/x-icon "access plus 1 week"
   # HTML no caching
   ExpiresByType text/html "access plus 0 seconds"
   # Other
   ExpiresByType application/xhtml-xml "access plus 1 month"
   ExpiresByType application/pdf "access plus 1 month"
   ExpiresByType application/x-shockwave-flash "access plus 1 month"
   ExpiresByType text/x-cross-domain-policy "access plus 1 week"
</IfModule>
#End of expires headers configuration


RewriteEngine On
RewriteRule ^.well-known/acme-challenge - [L]


# BEGIN Imagify: webp file type
<IfModule mod_mime.c>
  AddType image/webp .webp
</IfModule>
# END Imagify: webp file type


# BEGIN Force https
  RewriteEngine On
  RewriteCond %{HTTP:X-Forwarded-Proto} !https
  RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [L,R=301,NE]
# END Force https


# BEGIN Protect other domains trying to steal content
  RewriteCond %{HTTP_REFERER} !^https://(www\.)? [NC] 
  RewriteCond %{HTTP_REFERER} !^https://(www\.)?.*$ [NC]
# END Protect content (incl scorm) to be accessed from other domains


# BEGIN WordPress
# The directives (lines) between "BEGIN WordPress" and "END WordPress" are
# dynamically generated, and should only be modified via WordPress filters.
# Any changes to the directives between these markers will be overwritten.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress


# MalCare WAF
<Files ".user.ini">
  <IfModule mod_authz_core.c>
    Require all denied
  </IfModule>
  <IfModule !mod_authz_core.c>
    Order deny,allow
    Deny from all
  </IfModule>
</Files>
# END MalCare WAF


# Wordfence WAF
<Files ".user.ini">
  <IfModule mod_authz_core.c>
    Require all denied
  </IfModule>
  <IfModule !mod_authz_core.c>
    Order deny,allow
    Deny from all
  </IfModule>
</Files>
# END Wordfence WAF

The .htaccess file that I added in the subfolder:

<IfModule mod_expires.c>
   ExpiresActive On
   ExpiresDefault "access plus 6 hours"
   ExpiresByType text/css "access plus 6 hours"
</IfModule>

Update: I've removed the <IfModule mod_expires.c> wrapper. The mod_expires module is enabled/installed. The full header response is:

HTTP/2 200 OK
server: nginx
date: Fri, 14 Oct 2022 12:06:43 GMT
content-type: text/css
last-modified: Tue, 11 Oct 2022 19:41:25 GMT
vary: Accept-Encoding
etag: W/"63***425"
cache-control: public, max-age=9072000
content-encoding: gzip
X-Firefox-Spdy: h2

CodePudding user response:

still has the expiration header of max-age=9072000, I assume coming from the .htaccess file a folder up

Except that 9072000 seconds is 3.5 months (105 days), which doesn't appear to correspond to anything in the parent .htaccess file either.

server: nginx

From the HTTP response headers for this request to your .css file it would seem the response is being served by an Nginx server, not your Apache back-end/application server.

This suggests that you are behind a front-end proxy that is perhaps managing your static assets. This would also be responsible for the caching headers you are seeing. It's quite probable that Apache (your back-end server) is bypassed entirely for such requests. This is a relatively common optimization, as Nginx is more performant for serving static assets.

In fact, after a closer look at your .htaccess file I see you are checking the X-Forwarded-Proto HTTP response header in your "Force https" redirect - this confirms that you are indeed behind a (Nginx) proxy server. (If you weren't then that rule would result in a redirect loop, since that header is never normally set, except by proxy servers.)

If this is the case then you would need to make an exception in the proxy server that allows these requests through to be proccessed by your back-end/Apache server.

  • Related