Shoot yourself in the foot: WordPress UAM vs. HTTP Cache Headers [en]

Ever since I had reactivated this blog a couple of years ago, I noticed that something was wrong with its content caching. On each page reload, the browser would download all images from scratch. Even though it should have been easy to cache these images.

There are various standards govern how web browsers should cache resources. And, which HTTP response headers web servers can set to advise on desirable caching behavior.

I had kinda expected that the WordPress media system or the underlying Apache httpd server would set reasonable defaults for the most important cache headers. In particular, for static resources (like images) that are big and do not change often. But my WordPress did not send response headers like ETag or Last-Modified when serving images that I had uploaded.

As a work-around, I added a hard-coded Cache-Control header with a max-age=3600 directive to all HTTP responses that serve media. This is not a very efficient mechanism, because it limits caching to 1 hour and does not take changes to individual resources into account. But it seemed a good-enough compromise, and I ignored the remaining problem for several years.

Let’s revisit this…

I’m currently traveling Indonesia and I’ve been posting about it. Including heaps of photos, so this image caching problem started to bother me more and more. I decided to take some time and have another look at it.

I started with a quick web search for how to enable ETag headers in WordPress, but strangely I could not find any good results. Searches for wordpress caching yielded loads of hits about various server-side caches and on advanced cache headers tweaking. But nothing about simply getting the basics running.

Next, I searched for solutions at the Apache httpd server level and found its FileETag directive. Looks like this is a core part of Apache httpd and is active by default. This default generates ETags based on resource modification time and size in the underlying filesystem. But somehow I could not see any of these ETag headers in the HTTP responses.

So I checked my Apache httpd config (most of which is coming from the wordpress:apache Docker image) for FileETag overrides, but I couldn’t find any. I did find a .htaccess file that is part of my WordPress installation and directs most HTTP requests to WordPress’ own index.php script. However, this explicitly excludes resources that can be served directly from the underlying filesystem. These should still be handled by the Apache httpd.

But why didn’t it work for me? I finally decided to do some testing with a brand-new WordPress installation. This came with the same configuration in that .htaccess file, but to my surprise it served reasonable cache headers out-of-the-box. So I had to dig further, and figure out what’s different in the WordPress configuration of my blog…

What does the UAM plugin have to do with this?

Eventually, I found another .htaccess file in the wp-content/uploads/ directory, where the WordPress media system keeps all images and such. A quick look inside revealed that this config file directs HTTP requests to a script that is owned by the WordPress User Access Manager (UAM) plugin.

Ok, this finally made sense! I had installed the UAM plugin ages ago, because I wanted to share certain contents with friends only. That is, people with a user account for my blog. Obviously, the UAM plugin needs to intercept HTTP requests for media resources in order to run authorization checks.

A quick look into the UAM settings revealed that the UAM plugin had indeed installed this other .htaccess file. I even found a way to regenerate the file, but unfortunately this did not solve the cache headers problem.

Not sure why UAM does not set reasonable cache headers? Its authorization concept does not seem to prevent caching. Maybe UAM does support cache headers after all, but I clicked through its numerous settings and could not figure out how. Superficial search in the docs did not yield a solution either.

Bye-bye, UAM!

In the end, I decided to de-activate the UAM plugin. I don’t claim that this was the only solution, but it did work for me.

I only had a couple of posts (and about a hundred photos) that were protected by UAM authorization rules. These were all 15 years old, and I didn’t need them anymore. So I deleted them, cleaned up any dangling resources and configuration, then parted with the UAM plugin.

Now that the UAM plugin is de-activated, my WordPress blog is sending reasonable cache headers. In particular ETag or Last-Modified. Jumping back and forth between blog posts feels much smoother, now that the browser caches images. In the current setup, the browser still asks my WordPress server for changes before using a cached image. But these HTTP requests are lightweight and fast.

When it comes to sharing photos and other resources with friends, I don’t need to rely on WordPress and the UAM plugin anymore. In recent years I’ve been using my Nextcloud server instead, which has great file-sharing and access management capabilities.