Creating rules in .htaccess

Check your server first

The same .htaccess line can work on one host and answer 500 on every page of another. Before writing a rule, find out which server reads the file, how PHP runs and where TLS ends. A few short commands answer that. Tools used here: curl, SSH, grep.

The response headers name the server, but not the PHP mode:

curl -sSI https://example.com/

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

A probe file, opened in the document root, prints the PHP mode and the TLS variables. Give it a random name and delete it afterwards:

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

On a server you control, list the Apache modules. apache2ctl -M needs root or sudo and a server you control; a shared host usually gives no such command. This is a local Docker php:8.5-apache server with mod_php:

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)

On a FastCGI host (DreamHost), PHP runs as a separate process:

pgrep -l -u user php

2745746 php85.cgi

The Apache binary is there too:

ls /usr/sbin/apache2*

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

When a rule did break the site, the error log names the file and the directive:

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

sapi=cgi-fcgi with php85.cgi means PHP runs as a FastCGI process, not as mod_php: php_flag is unknown there and stops Apache with a 500. sapi=apache2handler means mod_php, where the same lines work. The full walk-through, the meaning of every answer and the safe test that .htaccess is read at all: Which server do you have.

Which rules are safe where

Read a row as: the directive group on the left, then what happens on each server. Safe means the rule works as written. Guard it means wrap it in an IfModule or If block, so a server without the module skips it instead of failing. Breaks the site means one unknown directive answers 500 for the folder and every folder below it. Ignored means the server never reads .htaccess.

Directive group Apache + mod_php Apache + FastCGI Apache behind a TLS proxy nginx alone LiteSpeed
Redirect / RedirectMatch safe safe absolute https:// target only: a bare path adds an http hop ignored safe
RewriteRule safe safe safe: build the scheme with an env variable ignored safe
php_flag / php_value safe breaks the site safe: depends on the PHP mode ignored check the host documentation
Header guard it: <IfModule mod_headers.c> guard it: <IfModule mod_headers.c> guard it: <IfModule mod_headers.c> ignored: use add_header in the server block safe
Options safe safe safe ignored guard it: check the host documentation
AddHandler / SetHandler for PHP guard it guard it: do not point PHP at mod_php guard it ignored guard it

A directive that AllowOverride does not allow in that folder (Options, for example) also answers 500.

The FastCGI and PHP-FPM entries follow the PHP manual on .user.ini files: php_value in .htaccess belongs to the Apache module mode, while CGI and FastCGI read .user.ini. The LiteSpeed rewrite rules page says LiteSpeed reads Apache-style rewrite rules. The per-user PHP settings page covers PHPRC and PHP_INI_SCAN_DIR and says .user.ini is not recommended with LSWS; it does not say that LiteSpeed takes php_value from .htaccess, so check the host documentation for PHP settings. The guards and a rollback plan are in Safe rules, guards and rollback.

Redirect: simple moves

One page moved to a new address. mod_alias handles it in one line - source first, target second:

Redirect 301 /old/page.php /new/page.php

Use 301 for permanent moves (link equity passes, browsers cache it) and 302 while experimenting. A cached wrong 301 is painful: browsers remember it long after you fix the file, so test with 302 and flip to 301 when sure.

On a host where HTTPS ends at a front proxy, a bare path target sends visitors to http:// first. The safe forms are in Redirect 301 or the pair and Redirects behind an HTTPS proxy.

RedirectMatch: patterns

Plain Redirect matches a URL prefix, which glues the remainder onto the target. To retire a whole tree into one file, anchor a regular expression instead:

RedirectMatch 301 ^/edu/suomen/.*$ /edu/index.php

Without the regex, /edu/suomen/x.php would land on /edu/index.phpx.php. Exact single-address matches work the same way:

RedirectMatch 301 ^/robot/?$ /qa/robot/

The /?$ anchor means subpaths like /robot/basics/ are untouched - only the bare directory redirects.

RewriteRule basics

When the target depends on part of the source, mod_rewrite captures it with parentheses and replays it as $1, $2:

RewriteEngine On RewriteRule ^blog/([0-9]{4})/([a-z-]+)\.php$ /archive/$1/$2/ [R=301,L]

RewriteRule sees the path without the leading slash and without the query string. To keep query parameters, add the QSA flag. A condition above the rule sees the full %{REQUEST_URI}, leading slash included, and captures groups as %1.

Flags that matter

Order of rules

Apache tries rules top to bottom, and a prefix-hungry rule shadows everything below it. Specific rules go first, catch-alls last:

Redirect 301 /pc/ /latex/ Redirect 301 /pc/LaTeX.php /latex/man/

Apache takes the first matching line, and the prefix match wins over the more specific line below it. /pc/LaTeX.php matches the /pc/ prefix, so the visitor lands on /latex/LaTeX.php, not /latex/man/: put the specific line first.

Comment the ordering constraint in the file, or the next editor will reorder it back. mod_rewrite rules run before mod_alias rules whatever the line order; do not mix the two in one file (why).

Testing rules

Never test in a browser first - the 301 cache lies. Ask for headers:

curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" https://example.com/robot/

301 https://example.com/qa/robot/

Check the chain, not just the first hop: two redirects in a row double the latency and leak ranking. One hop from old URL to final page is the goal.

Common pitfalls

Keep the same discipline as with code: one rule per move, a comment above non-obvious ones, and a periodic audit of every target.

The old chapter about HTTPS proxies now has its own article: Redirects behind an HTTPS proxy.

The topic articles

  1. Which server do you have - response headers, a PHP probe, Apache modules, the PHP process, the error log and the safe test that .htaccess is read at all.
  2. Safe rules, guards and rollback - backup, test in a subfolder, IfModule and If guards, and where PHP settings go when php_flag cannot be used.
  3. Redirect 301 or the pair - when a one-line Redirect is right and when the RewriteCond plus RewriteRule pair is the safe choice.
  4. Redirects behind an HTTPS proxy - why a proxied Apache writes http:// and how this site converted every redirect to a mod_rewrite pair.

Article author: Andrei Olegovich