The short version. I wanted to know whether a block rule in a .htaccess file was doing anything. I asked for a file that did not exist, with the extension the rule claims to stop, and got 404. I concluded the rule was dead, wrote it up, and told a colleague.
The rule was alive. My probe returns 404 whether the rule works or not, because a rewrite rule higher up the configuration answers first and stops everything after it.
The test that distinguishes takes one more step and I should have run it first: ask for a file that exists.
What I was trying to find out
A directory on this site holds uploaded images. Since images are the only thing that belongs there, a .htaccess in that directory forbids requests for anything that could execute — .php and a list of relatives — with mod_rewrite’s [F] flag.
The question was whether that file was doing its job. This server is OpenLiteSpeed, not Apache, and .htaccess is an Apache mechanism that OpenLiteSpeed supports for compatibility. Support for a mechanism is not the same as identical behaviour, so the file existing tells you nothing on its own.
That framing was right. My method for answering it was not.
The probe that cannot fail
Apache’s documentation on rewrite flags says what [F] does: “Using the [F] flag causes the server to return a 403 Forbidden status code to the client.” And the technical notes place per-directory rewriting in the request cycle: it runs after the per-directory configuration files have been read, and before the handler that would eventually decide whether the file is there.
So the reasoning went: a forbidding rule fires before anyone looks for the file, therefore it should return 403 even for a filename I just invented. Ask for something that certainly does not exist; 403 means the rule is alive, 404 means it is not. No file uploaded, nothing left behind, ten seconds.
I ran it against six extensions. All six returned 404. I took that as an answer.
Why it was not an answer
The virtual host on this server carries WordPress’s front-controller rules at host level, not in a file inside the document root:
| Rule | What it does for an invented path |
|---|---|
RewriteCond %{REQUEST_FILENAME} !-f | true — no such file |
RewriteCond %{REQUEST_FILENAME} !-d | true — no such directory |
RewriteRule ^/?(.*)$ /index.php [L] | matches, rewrites to the front controller, stops |
Both conditions are satisfied precisely because the file does not exist — which is the one property my probe was built on.
Two things follow, and together they explain everything.
Host-level rules run before the rules attached to a directory. And OpenLiteSpeed’s [L] is stronger than Apache’s: its own documentation equates it to Apache’s [END], which stops the rewrite engine entirely rather than restarting the round. So for an invented path the front-controller rule matched, ended the engine, and the directory’s rule never ran. WordPress received a request for a page it does not have and answered 404, correctly.
That 404 is produced identically whether the block rule is alive or dead. My probe could not distinguish the two situations it was built to distinguish.
The worst part is that the explanation was already written down. A comment I put in that same configuration file five days earlier says: ojo: la catch-all de abajo lleva [L] y corta — the catch-all below carries [L] and cuts. I wrote the reason, and then ran a test that the reason invalidates.

The test that does distinguish
Invert the one condition the front-controller rule depends on: ask for a file that exists.
Choose an extension the rule covers but that cannot execute on that server — on this one a .pl holds plain text and nothing will run it — put one byte of text in it, request it, and delete it.
| Request | Result | What it establishes |
|---|---|---|
| File that exists, covered extension | 403 | The rule is alive and fires |
| File that does not exist, same extension | 404 | Nothing — the front controller answered |
| A real image in the same directory | 200 | Control: the directory is reachable |
Measured through the CDN and directly against the origin; both gave 403. The rule I had declared dead was working the whole time.
With the file present, !-f is false, the front-controller rule does not match, the engine is not stopped, and the directory’s rule finally gets to run. It returns 403, which is what it was written to do.
Telling “this is blocked” apart from “something else answered first” is most of what the free check does before it reports anything, with the requests written out so you can repeat them. Reading a whole stack that way — host rules, directory rules, CDN rules and the order they fire in — is the technical SEO and server work.
What the file on disk does and does not tell you
The framing I started with was right, and it is worth stating properly now that I have the measurement.
Apache reads these files continuously. Its documentation is explicit: “the .htaccess file is loaded every time a document is requested”. Edit the file, and the next request uses the new version.
OpenLiteSpeed does it once. Its rewrite documentation describes the setting that enables the behaviour — autoload rewrite rules contained in a directory’s .htaccess “when first accessing that directory” — and states the consequence plainly: “Once initially loaded, a graceful restart must be performed for any further changes to that .htaccess file to take effect.”
So under OpenLiteSpeed the file on disk is not the configuration. It is the source the configuration was built from, at some point in the past. The two can differ, and nothing about the file’s presence, timestamp or contents tells you which version the server is holding.
On this server the loading is visible: the error log records the directory’s file being parsed, once per worker process, with the most recent entry dated the day before I ran my probe. The evidence that the rule was loaded was sitting in a log I had not read, while I was concluding from a status code that it was not.
So is the probe useless?
No, but it is narrower than I published, and the distinction is worth having.
Under Apache, per-directory configuration for the deepest directory generally supersedes what came before, so asking for a non-existent path with a covered extension usually does distinguish 403 from 404. That is where the technique comes from and there it works.
Under OpenLiteSpeed with rewrite rules at host level — which is to say almost any WordPress on this kind of stack — an invented path is captured by the front controller before anything else runs, and the probe returns 404 regardless. Publishing it as a universal thirty-second test would have handed readers a method that fails on the first site they try it on.
It does still work for a different kind of protection: one expressed as a host-level access rule rather than a rewrite. Those are evaluated against the original request in a different phase, which the front controller does not pre-empt. Same symptom, same status code, two mechanisms, and only one of them is visible to the shortcut.
What I have not established
Three things, kept separate from the parts that are measured.
I have not tested a file that can execute. Demonstrating that would mean putting a working script into a live document root, and the argument for doing it is never good enough. The measurement above uses a file the server has no handler for, which is enough to show the rule fires and not enough to describe what would happen to something executable.
I have not established when this directory’s rule was first loaded, only that it has been loaded recently. Under a load-once model that distinction matters, and a log only shows you the loads that happened while it was being kept.
And I am not recommending a configuration change off the back of this. Moving these protections into host-level rules is the arrangement the vendor documents for restricting access to a directory, and it would also survive an edit to the file going unnoticed. It also requires restarting the server, which on a shared machine affects every site on it — so it is a proposal with a cost, not a conclusion, and it is not mine alone to make.
The method, and the places where it stops being able to tell you anything, is written out in how I measure. If you would rather have your own stack read than read about mine, send me a domain.
Block rules and .htaccess under OpenLiteSpeed: frequently asked questions
How do I check whether a [F] rule is actually working?
Request a file that exists and is covered by the rule, choosing an extension the server cannot execute. A 403 means the rule fires. Asking for a filename you invented is not a reliable test wherever front-controller rewrite rules run before it.
Why did my invented path return 404 instead of 403?
Most likely a front-controller rule matched it first. Those rules typically require that the request does not correspond to an existing file or directory, which is exactly true of an invented path, and they usually end with a flag that stops the rewrite engine.
Does OpenLiteSpeed support .htaccess?
Yes, with a setting that loads a directory’s file when that directory is first accessed. The documented difference from Apache is when it is read: Apache reads it on every request; OpenLiteSpeed loads it once and requires a graceful restart for later edits to take effect.
If I edit a .htaccess on OpenLiteSpeed, does the change apply?
Not necessarily to a directory already loaded. The documentation says a graceful restart must be performed for further changes to that file to take effect. This is the practical reason the file on disk is not proof of what the server is enforcing.
Can I tell from the file itself whether it is in effect?
No. The file’s presence, its contents and its timestamp are all consistent with it being loaded, loaded in an older version, or not yet loaded for that directory. The server’s error log is more informative than the file.
Is there a way to protect a directory that does not depend on this?
Yes — express the restriction in the host configuration rather than in a file inside the document root. That is what the vendor’s documentation describes for restricting access to a directory. The trade-off is that changing host configuration requires a server restart.
The general shape of this
The test was not careless. It was built on a correct reading of what a forbidding rule does and when it runs, and in the environment the technique comes from it is a good test. It failed for a reason that sat one layer above the layer I was looking at, in a file I had edited myself.
What I take from it is a question rather than a rule, because the rule would be too long: before believing a status code, ask what else could produce the same one. If the answer is “something that has nothing to do with what I am testing”, the measurement is not evidence yet, however many times it repeats. Mine repeated six times, which is the same trap I had already written about from the other direction and did not recognise wearing different clothes.
The rule was fine. The file was fine. The only broken thing was the instrument, and it was broken in the one way instruments are hardest to catch: it produced a plausible answer every single time.
Plausible output hides damage too: PHP’s strip_tags once deleted three quarters of a post over one character, and every layer reported success.


