All posts

The IfModule trap on LiteSpeed: the homepage works, everything else 404s

We deployed the site to Hostinger. The homepage came up, static files were served, PHP ran. But /projects, /blog, /admin/login — all 404.

The wrong suspects

Three explanations came to mind first: .htaccess never reached the server, mod_rewrite is off, or the directory layout is wrong. All three were reasonable. All three were wrong.

There is one question that saves time in cases like this: who is returning the 404 — the web server or the application?

The headers answer it:

$ curl -sSI https://bulud.tech/admin/login
HTTP/2 404
last-modified: Tue, 22 Apr 2025 07:41:12 GMT
server: LiteSpeed

That last-modified is a year old. So this is not a page the application generated — it is LiteSpeed's own 404 file sitting on disk. PHP is never invoked.

For comparison, the application's own 404 carried content-type: text/html; charset=UTF-8, a session cookie and our security headers. A completely different response.

The decisive evidence

The real answer came from an unexpected place — the headers of an ordinary CSS file:

$ curl -sSI https://bulud.tech/assets/css/styles.css
HTTP/2 200
cache-control: public, max-age=604800

cache-control is there. But x-frame-options and x-content-type-options are not — even though both were written in .htaccess.

That difference is the tell. The headers sat inside an <IfModule mod_headers.c> block. The routing rules sat inside <IfModule mod_rewrite.c>. Both blocks were being skipped.

The cause

LiteSpeed reads Apache's .htaccess syntax, but it does not evaluate <IfModule> the same way: module names are not registered in its internal table the way they are in Apache. The condition comes out false and the whole block is ignored — with no error and nothing in the log.

The homepage working was no coincidence: / resolves through DirectoryIndex straight to the index.php file on disk. It needs no rewrite. Every virtual URL does.

The fix

Take the critical directives out of the conditional:

DirectoryIndex index.php

RewriteEngine On
RewriteBase /

RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]

RewriteRule ^ index.php [L]

The <IfModule> guard exists to avoid a 500 when a module is absent. But a site like this cannot work at all without mod_rewrite — so the guard protects nothing; it only makes the failure silent.

What we took away

Silent failure is the most expensive kind. An <IfModule> block says "skip quietly if the module is missing"; the problem is that "I don't recognise this module" and "this module is missing" are not the same thing.

The most useful diagnostic habit: establish who answered. A static file's headers show it in a single request — because no PHP is involved, everything you see is the server's own configuration.