Which server do you have: check before writing .htaccess

Response headers

Start with one request and read the server header. Tools used here: curl, SSH, grep.

curl -sSI https://example.com/

HTTP/2 200 content-type: text/html; charset=UTF-8 server: Apache

The header is a hint, not a diagnosis. A proxy in front hides the real server, and a host may replace or drop the header. It does not tell you whether PHP runs as mod_php or as a separate process, and that is the difference that turns a working .htaccess into a 500.

A probe PHP file

A tiny PHP file answers the rest. Give it a name nobody guesses, and delete it when you are done:

vi ~/example.com/probe-7f3a.php

<?php echo "HTTPS=", var_export($_SERVER['HTTPS'] ?? null, true), " XFP=", var_export($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null, true), " SERVER_SOFTWARE=", $_SERVER['SERVER_SOFTWARE'] ?? '', " sapi=", PHP_SAPI, "\n";

curl -sS https://example.com/probe-7f3a.php

On a FastCGI host (DreamHost) the probe answers:

HTTPS='on' XFP=NULL SERVER_SOFTWARE=Apache sapi=cgi-fcgi

rm ~/example.com/probe-7f3a.php

In that answer HTTPS (TLS) ends in Apache (HTTPS=on), no proxy header is present (XFP=NULL) and PHP is not an Apache module (sapi=cgi-fcgi). The next section explains the three SAPI values.

What PHP_SAPI means

ValueHow PHP runsWhat it means for .htaccess
apache2handler mod_php inside Apache php_flag and php_value work.
cgi-fcgi a CGI or FastCGI process, often mod_fcgid or a wrapper such as php85.cgi php_flag is unknown: guard it or move the setting to .user.ini or the host file.
fpm-fcgi PHP-FPM php_flag is unknown: use .user.ini or the pool configuration.
cli the command line, not a web request No .htaccess at all: a probe run from a shell proves nothing about the web server.

The same values decide where PHP settings belong. mod_php reads php_flag from .htaccess; CGI, FastCGI and PHP-FPM read .user.ini or the ini file of the host. Safe rules has the exact files and the restart commands.

Apache modules

On a local Docker php:8.5-apache server with mod_php, ask Apache itself which modules are loaded:

apache2ctl -M | grep -i -E 'php|rewrite|alias|headers|version'

version_module (static) alias_module (shared) headers_module (shared) php_module (shared) rewrite_module (shared)

php_module (shared) is mod_php. There is no php_module line on a FastCGI host, even though Apache is still Apache. alias_module is needed by Redirect and RedirectMatch, rewrite_module by RewriteRule, headers_module by Header, and version_module by IfVersion. A missing module is exactly what the guards in Safe rules are for. A shared host usually gives no apache2ctl, so there the probe file and the host panel are the only answer.

Which PHP process runs

When Apache is only the front end, the PHP process has its own name. On a FastCGI host (DreamHost) it looks like this:

pgrep -l -u user php

2745746 php85.cgi

ls /usr/sbin/apache2*

/usr/sbin/apache2 /usr/sbin/apache2ctl

php85.cgi is a FastCGI worker, so PHP is not inside Apache. A host that runs PHP-FPM shows a process named php-fpm instead. The presence of apache2ctl confirms Apache, but it does not confirm mod_php.

Where the error log is

A broken rule is not silent. Apache writes the file name and the directive to its error log:

tail -n 1 ~/logs/example.com/https/error.log

[Fri Nov 06 23:29:54 2026] [example.com] [alert] [client 203.0.113.7:37118] [pid 3064457] config.c(2045): /home/user/example.com/.htaccess: Invalid command 'php_flag', perhaps misspelled or defined by a module not included in the server configuration

On a shared host the log usually sits in the home folder, one folder per protocol; on Debian and Ubuntu it is often /var/log/apache2/error.log; a control panel shows the path in its log viewer. Ask the host if none of these exist. The line config.c means Apache stopped while reading its configuration, and the path after it names the exact file. A real case of this 500 is in PHP errors: case studies.

Is .htaccess read at all

nginx never reads .htaccess, and Apache can be told to ignore it with AllowOverride None. Test in a throw-away subfolder, never on the home page. A deliberate bad line answers 500 only there:

mkdir -p ~/example.com/test-htaccess

vi ~/example.com/test-htaccess/.htaccess

ThisDirectiveDoesNotExist on

On a local Apache 2.4.68 with mod_php, the test folder /test-htaccess/ on http://localhost:8080 answers 500, while its parent still answers 200:

curl -sS -o /dev/null -w "%{http_code}\n" http://localhost:8080/test-htaccess/

500

curl -sS -o /dev/null -w "%{http_code}\n" http://localhost:8080/

200

The 500 in the test folder and the 200 in its parent mean the server reads .htaccess here: the bad line is the only cause. A 500 on both points at the server configuration instead; 200 on both means .htaccess is ignored in this folder, and no redirect written there can work. Remove the test folder either way:

rm -rf ~/example.com/test-htaccess

A redirect is the other safe probe: add one rule in the test folder and ask for its source with curl -sI. A 301 proves the file is read; a 404 proves it is not. This test changes the answer of that one folder only.

Where TLS ends

The probe answers this without a guess:

The full story of a proxied Apache, the extra http hop and the two conditions that stop a loop is in Redirects behind an HTTPS proxy.

Article author: Arthur Isaev