How the HTTPS Redirect Rule Works
RewriteCond %{HTTPS} !=on [NC]
RewriteCond %{HTTP:X-Forwarded-Proto} !https [NC]
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
Role of Each Line
Line 1:
RewriteCond %{HTTPS} !=on [NC]
Checks that the current request is not an HTTPS connection. If the client is already connected directly via HTTPS, this condition does not match and no redirect occurs.
Line 2:
RewriteCond %{HTTP:X-Forwarded-Proto} !https [NC]
Handles cases where the request passes through a reverse proxy or CDN (Cloudflare, AWS ALB, etc.).
In the following topology:
User →(HTTPS)→ CDN/Proxy →(HTTP)→ Server
The server sees an HTTP request even though the user is accessing over HTTPS. The proxy sets the
X-Forwarded-Proto
header, which allows the original protocol to be determined.
Without this line, proxy-forwarded HTTPS requests will trigger an infinite redirect loop .
Line 3:
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
Only when both conditions above are true (i.e., it is a genuine HTTP request) does a 301 redirect to the HTTPS URL take place.
RewriteCond (Condition Directive)
A conditional statement that determines "whether to execute the next
RewriteRule
". Equivalent to an
if
statement in programming.
Syntax
RewriteCond %{TestString} Pattern [Flags]
Common Test Strings (Server Variables)
| Variable | Description |
|---|---|
%{HTTPS} |
on
when connection is HTTPS
|
%{HTTP_USER_AGENT} |
User-Agent string of the browser or bot |
%{REQUEST_URI} |
Requested path (e.g.
/wp-admin/
)
|
%{REQUEST_FILENAME} |
Actual file path on the server |
%{QUERY_STRING} |
Query string after
?
|
%{THE_REQUEST} |
Raw HTTP request line (e.g.
GET /path HTTP/1.1
)
|
%{HTTP:HeaderName} |
Value of any HTTP request header |
Why Use
%{THE_REQUEST}
and When
%{THE_REQUEST}
and
%{REQUEST_URI}
look similar, but there is a critical difference:
before vs. after Apache's URL normalization
.
| Variable | Example value | Characteristic |
|---|---|---|
%{REQUEST_URI} |
/path/to/page |
Path after Apache normalization —
//
is collapsed to
/
|
%{THE_REQUEST} |
GET /path//to/page HTTP/1.1 |
Raw request line sent by the client; retains pre-normalization information |
Detecting double slashes
requires
%{THE_REQUEST}
. By the time
%{REQUEST_URI}
is available,
//
has already been collapsed to
/
and the condition will not match.
# Normalize URLs containing double slashes with a 301 redirect
# %{REQUEST_URI} is already normalized, so use %{THE_REQUEST} for detection
RewriteCond %{THE_REQUEST} \s[^\s?]*//
RewriteRule ^ %{REQUEST_URI} [R=301,L,NE]
Breakdown of the
\s[^\s?]*//
pattern:
-
\s— Whitespace between the method and the URL (immediately afterGET) -
[^\s?]*— Any characters except whitespace and?(targets the path portion only) -
//— The double slash being detected
The
RewriteRulepattern is applied against the normalizedREQUEST_URI. Use%{THE_REQUEST}to detect double slashes in the original request line, then specify the already-normalized%{REQUEST_URI}as the redirect target. TheNEflag (noescape) prevents special characters in the redirect target from being escaped. This is the same pattern used in the generator output.
Pattern Syntax
# Regex match
RewriteCond %{HTTP_USER_AGENT} (wget|curl|nikto) [NC]
# Negation match (prefix with !)
RewriteCond %{HTTP:X-Forwarded-Proto} !https [NC]
# Check if a file exists
RewriteCond %{REQUEST_FILENAME} -f
# Check if a directory exists
RewriteCond %{REQUEST_FILENAME} -d
Test Operators (-f, -d, etc.)
In addition to regular expressions, the pattern portion accepts special test operators that check file system state.
| Operator | Meaning | What it checks |
|---|---|---|
-f |
is F ile | A regular file exists |
-d |
is D irectory | A directory exists |
-s |
is file with S ize | File exists and has a non-zero size |
-l |
is symbolic L ink | A symbolic link exists |
-F |
is existing F ile (via subrequest) | Existence confirmed via an Apache subrequest (expensive) |
-U |
is existing U RL (via subrequest) | URL is accessible (expensive) |
In practice,
-fand-dare by far the most common. Prefix with!to negate (e.g.!-f= file does not exist,!-d= directory does not exist). WordPress.htaccessfiles frequently combine!-fand!-d.
WordPress Usage Example
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
This means "if the requested path corresponds to an actual file or directory, return it as-is without rewriting the URL."
This is necessary to serve static files (images, CSS, JS) directly without routing them through WordPress's rewrite engine (
index.php
). Without it, every request to a real file such as
/wp-content/uploads/photo.jpg
would be forwarded to
index.php
, breaking images and stylesheets.
Combining Multiple Conditions
Multiple
RewriteCond
lines default to an
AND
relationship.
# Condition A AND Condition B → RewriteRule fires only when both are satisfied
RewriteCond %{HTTPS} !=on [NC]
RewriteCond %{HTTP:X-Forwarded-Proto} !https [NC]
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
Use the
[OR]
flag to create an OR relationship.
# File exists OR directory exists
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
Important: A
RewriteCondapplies only to theRewriteRuleimmediately following it.
RewriteRule (Rewrite Rule)
The core directive that actually rewrites URLs or issues redirects.
Syntax
RewriteRule Pattern Substitution [Flags]
Pattern and Substitution
| Element | Description |
|---|---|
^(.*)$ |
Matches any URL (captured with
()
)
|
$1 |
References the captured content in the substitution |
%{HTTP_HOST} |
Hostname (e.g.
example.com
)
|
%{REQUEST_URI} |
Request path |
- |
No URL rewrite (used for blocking) |
Examples
# HTTPS redirect
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
# Return 403 Forbidden without rewriting the URL (bot blocking)
RewriteRule .* - [F,L]
# Return 410 Gone (against malicious query strings)
RewriteRule ^ - [R=410,L]
IfModule (Module Existence Check)
Executes the enclosed directives only when the specified Apache module is enabled.
Syntax
<IfModule ModuleName>
# Executed only when the module is available
</IfModule>
Why It Is Needed
Writing directives for a module that does not exist causes an
Apache startup error (500)
. Wrapping them in
IfModule
allows the block to be skipped safely in environments where the module is absent.
Common Modules
| Module | Role |
|---|---|
mod_rewrite.c |
URL rewriting and redirects |
mod_deflate.c |
Gzip compression |
mod_expires.c |
Browser cache expiration headers |
mod_headers.c |
Adding/modifying HTTP response headers |
mod_authz_core.c |
Access control (Apache 2.4+) |
mime_module |
Adding MIME types |
Negation (Apache 2.2 / 2.4 Compatible Pattern)
<IfModule mod_authz_core.c>
# Apache 2.4+ syntax
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
# Apache 2.2 and earlier (fallback)
Order deny,allow
Deny from all
</IfModule>
Note: XServer runs Apache 2.4, so in practice only the
mod_authz_core.cblock is used.
Module Enable Switches
Some modules require not just an
IfModule
wrapper but also an explicit
enable switch (directive)
to activate the feature. Without the switch, all subsequent settings in the block are silently ignored.
Modules That Require a Switch
| Module | Switch | Role |
|---|---|---|
mod_rewrite.c |
RewriteEngine On |
Enables the URL rewriting engine |
mod_expires.c |
ExpiresActive On |
Enables the cache expiration feature |
# For mod_rewrite
<IfModule mod_rewrite.c>
RewriteEngine On ← Switch ON
RewriteCond %{HTTPS} !=on [NC]
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</IfModule>
# For mod_expires
<IfModule mod_expires.c>
ExpiresActive On ← Switch ON
ExpiresByType text/css "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 month"
</IfModule>
Modules That Do Not Need a Switch
Modules such as
mod_deflate
and
mod_headers
do not require a switch — directives can be written directly inside the
IfModule
block. Not every module has an enable switch; it depends on the module's specification.
Basic Pattern
IfModule checks module exists → Switch ON → Write directives
These three steps together are the standard
.htaccess
boilerplate.
Note: You can also explicitly disable with
RewriteEngine Off, which is useful for temporarily stopping all rewrite rules during debugging.Note: When multiple
<IfModule mod_rewrite.c>blocks exist, each block needs its ownRewriteEngine On. Each block is independent, so the switch must be repeated.
RewriteCond Flags
| Flag | Name | Description |
|---|---|---|
[NC] |
No Case | Case-insensitive matching |
[OR] |
OR condition | Changes the default AND relationship to OR |
RewriteRule Flags
Frequently Used Flags
| Flag | Name | Description |
|---|---|---|
[L] |
Last |
Stops the current rewrite pass; no further rules are evaluated. However, if the URI was rewritten, Apache internally re-evaluates
.htaccess
. When combined with
[R]
(external redirect), a response is sent to the client and processing is complete, so no re-evaluation occurs.
|
[R=code] |
Redirect |
Issues a redirect with the given HTTP status code (e.g.
R=301
,
R=410
). Using
[R]
alone defaults to 302.
|
[F] |
Forbidden | Returns 403 Forbidden |
[NE] |
No Escape | Does not encode the substitution URL (prevents double-encoding) |
[T=type] |
Type |
Forces a MIME type (e.g.
T=image/webp
)
|
Other Flags
| Flag | Name | Description |
|---|---|---|
[QSA] |
Query String Append | Appends the original query string to the redirect destination |
[QSD] |
Query String Discard | Discards the query string |
[P] |
Proxy | Forwards the request as a proxy instead of redirecting |
[G] |
Gone |
Returns 410 Gone (shorthand for
[R=410]
)
|
[CO] |
Cookie | Sets a cookie |
[E] |
Environment | Sets an environment variable |
Combining Flags
Multiple flags can be combined with comma separation.
# 301 redirect + stop processing + no encoding
RewriteRule ^ %{REQUEST_URI} [R=301,L,NE]
Always include
[L]. Omitting it can cause subsequent rules to be applied unintentionally.
Relationship Between the Three Directives
IfModule → "Is this module available?"
└ RewriteCond → "Does this condition match?"
└ RewriteRule → "Condition met — process it like this"
Reference: HTTP Status Codes Used in .htaccess
| Code | Meaning | Use case |
|---|---|---|
| 301 | Moved Permanently | Permanent redirect (HTTPS enforcement, URL normalization) |
| 302 | Found | Temporary redirect |
| 403 | Forbidden | Access denial (bot blocking, file protection) |
| 404 | Not Found |
Resource does not exist (use
ErrorDocument 404
for a custom error page)
|
| 410 | Gone | Resource permanently removed (used against malicious requests) |