Testing and Debugging in .htaccess
Writing .htaccess rules is only half the battle. The other half is making sure they actually work the way you intended. A misplaced character, a missing module, or a subtle logic error in a rewrite rule can bring your entire site down with a cryptic 500 Internal Server Error or trap visitors in an infinite redirect loop.
This guide gives you a complete toolkit for testing and debugging .htaccess files. You will learn how to check for syntax errors before they cause problems, how to read Apache's error logs to pinpoint issues, how to diagnose and fix the most common mistakes, how to enable detailed rewrite logging, and how to structure a safe workflow for moving rules from development to production.
Checking for Syntax Errors
The first line of defense against .htaccess problems is catching syntax errors before they affect live traffic.
apachectl configtest
Apache provides a built-in configuration validation command:
sudo apachectl configtest
If your main server configuration files are valid, the output will be:
Syntax OK
If there is an error, Apache reports the file and line number:
AH00526: Syntax error on line 12 of /etc/apache2/sites-enabled/000-default.conf:
Invalid command 'RewriteEngne', perhaps misspelled or defined by a module not included in the server configuration
This is extremely useful because it catches typos and missing modules before you restart Apache, preventing downtime.
apachectl configtest only validates the main server configuration files (httpd.conf, virtual host files, and included files). It does not check .htaccess files. Syntax errors in .htaccess files are only detected when Apache processes a request that triggers the file. This is why testing after every change is so important.
An Alternative: apache2ctl
On Debian and Ubuntu systems, apache2ctl is the equivalent command:
sudo apache2ctl configtest
Both commands work identically. Use whichever one is available on your system.
Testing .htaccess Syntax Indirectly
Since apachectl configtest does not cover .htaccess files, you need another approach. The most reliable method is to make your change and then immediately request a page from the affected directory:
curl -I http://localhost/
The -I flag sends a HEAD request and displays only the response headers. If you see a normal response like HTTP/1.1 200 OK, your .htaccess file is syntactically valid (at least for that request). If you see HTTP/1.1 500 Internal Server Error, there is a problem.
You can also use curl with the -v (verbose) flag for more detail:
curl -v http://localhost/test-page
This shows the full request and response, including redirect chains, headers, and status codes, making it much more informative than a browser for debugging.
Reading Apache Error Logs
When something goes wrong with an .htaccess file, the error log is where Apache tells you exactly what happened. Learning to read this log is the single most valuable debugging skill you can develop.
Log File Locations
The error log location varies by operating system and configuration:
| System | Default Error Log Path |
|---|---|
| Ubuntu / Debian | /var/log/apache2/error.log |
| CentOS / RHEL / Fedora | /var/log/httpd/error_log |
| macOS (Homebrew) | /usr/local/var/log/httpd/error_log |
| XAMPP (Windows) | C:\xampp\apache\logs\error.log |
| XAMPP (macOS) | /Applications/XAMPP/logs/error_log |
If you are unsure where your logs are, check your Apache configuration for the ErrorLog directive:
grep -r "ErrorLog" /etc/apache2/
Monitoring Logs in Real Time
The most effective way to debug .htaccess issues is to watch the error log in real time while you make requests:
sudo tail -f /var/log/apache2/error.log
The -f (follow) flag keeps the log open and streams new entries as they appear. Open this in one terminal window, then use another window or your browser to make requests. Errors will appear instantly.
Understanding Log Entries
A typical .htaccess error log entry looks like this:
[Wed Oct 15 14:32:01.456789 2024] [core:alert] [pid 12345] [client 127.0.0.1:54321]
/var/www/html/.htaccess: Invalid command 'RewriteEngne', perhaps misspelled or defined
by a module not included in the server configuration
Breaking this down:
| Part | Meaning |
|---|---|
[Wed Oct 15 14:32:01...] | Timestamp of the error. |
[core:alert] | The module that reported the error (core) and the severity level (alert). |
[pid 12345] | The process ID of the Apache worker that encountered the error. |
[client 127.0.0.1:54321] | The IP address and port of the client whose request triggered the error. |
/var/www/html/.htaccess | The exact file where the error was found. |
Invalid command 'RewriteEngne'... | The human-readable error description. |
In this example, the message is clear: RewriteEngne is misspelled (it should be RewriteEngine), or the module providing it is not loaded.
Common Error Messages and Their Meanings
Here are error messages you will encounter frequently:
Invalid command:
Invalid command 'RewriteEngine', perhaps misspelled or defined by a module
not included in the server configuration
The directive name is wrong, or the required module is not enabled. In this case, mod_rewrite needs to be enabled.
Not allowed here:
/var/www/html/.htaccess: Options not allowed here
The AllowOverride setting does not permit this type of directive. The AllowOverride value needs to include Options (or be set to All).
Option not permitted:
/var/www/html/.htaccess: Option FollowSymLinks not allowed here
Similar to the above, but specific to the Options directive. The AllowOverride needs to include Options.
Regular expression error:
Compilation failed: missing terminating ] for character class
A regular expression in a RewriteRule or RewriteCond has a syntax error.
Common Mistakes
Certain .htaccess mistakes come up again and again. Learning to recognize and fix them quickly will save you significant debugging time.
500 Internal Server Error
The 500 Internal Server Error is the most common result of an .htaccess problem. It means Apache encountered something in your .htaccess file that it cannot process, and rather than serving a potentially broken page, it returns a generic error.
Common causes:
1. Typos in directive names:
# Wrong: misspelled directive
RewriteEngne On
# Correct
RewriteEngine On
2. Using a directive from a module that is not loaded:
# Fails if mod_rewrite is not enabled
RewriteEngine On
RewriteRule ^about$ /about.html [L]
Fix: Enable the module or wrap the directives in an <IfModule> block:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^about$ /about.html [L]
</IfModule>
3. Using a directive not allowed by the current AllowOverride setting:
# Fails if AllowOverride does not include Options
Options -Indexes
Fix: Update AllowOverride in the main configuration or remove the directive.
4. Invalid regular expression syntax:
# Wrong: unescaped dot and missing closing bracket
RewriteRule ^file[.html$ /file.html [L]
# Correct: properly escaped and closed
RewriteRule ^file\.html$ /file.html [L]
5. Invalid characters or encoding:
If you copy-paste directives from a website or a word processor, invisible characters (smart quotes, non-breaking spaces, byte order marks) can end up in the file. These cause parsing errors that are extremely hard to spot visually.
Fix: Always type directives manually or paste into a plain text editor first. Ensure the file is saved as UTF-8 without BOM with Unix line endings (LF).
When you encounter a 500 error and are not sure which line is causing it, use a binary search approach: comment out the bottom half of your .htaccess file, reload, and see if the error persists. If it does, the problem is in the top half. Keep halving until you isolate the offending line.
# Uncommented: testing these first
RewriteEngine On
RewriteRule ^about$ /about.html [L]
# Temporarily commented out
# Header set X-Frame-Options "DENY"
# ErrorDocument 404 /errors/404.html
Infinite Redirect Loops
An infinite redirect loop occurs when a rewrite or redirect rule keeps firing repeatedly, redirecting the browser back to the same URL or between two URLs endlessly. The browser eventually gives up and shows an error like ERR_TOO_MANY_REDIRECTS.
Common cause: redirecting without a condition check.
Wrong:
RewriteEngine On
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
This rule matches every request, including requests that are already on HTTPS. Apache redirects to HTTPS, the browser makes a new request over HTTPS, the rule matches again, Apache redirects again, and the loop continues forever.
Correct:
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
The RewriteCond %{HTTPS} off ensures the redirect only fires for HTTP requests. HTTPS requests pass through without triggering the rule.
Another common cause: www redirect loop.
Wrong:
RewriteEngine On
RewriteRule ^ https://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
If the user visits www.example.com, the rule prepends another www., redirecting to www.www.example.com, then www.www.www.example.com, and so on.
Correct:
RewriteEngine On
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^ https://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
The condition checks that the hostname does not already start with www. before adding it.
Debugging redirect loops:
Use curl with the -L (follow redirects) and -v (verbose) flags to trace the chain:
curl -LvI http://example.com/test-page 2>&1 | grep -E "< HTTP|< Location"
This shows each redirect step:
< HTTP/1.1 301 Moved Permanently
< Location: https://example.com/test-page
< HTTP/1.1 301 Moved Permanently
< Location: https://example.com/test-page
...
If you see the same Location header repeating, you have found your loop.
Browsers aggressively cache 301 (permanent) redirects. If you accidentally created a redirect loop with a 301 status and then fixed your .htaccess, your browser may still follow the cached redirect. Clear your browser cache or test in an incognito/private window after fixing redirect issues.
Wrong File Permissions
Apache must be able to read the .htaccess file to process it. If the file permissions are too restrictive, Apache silently ignores the file as if it does not exist.
Symptoms of a permission problem:
- Your
.htaccessdirectives have no effect, but there are no errors in the log. - The same directives work when placed in the main server configuration.
Check current permissions:
ls -la /var/www/html/.htaccess
Output:
-rw------- 1 root root 245 Oct 15 14:30 .htaccess
In this case, the file is owned by root and only root can read it. If Apache runs as the www-data user (common on Ubuntu/Debian), it cannot read the file.
Fix: set appropriate permissions:
chmod 644 /var/www/html/.htaccess
This gives:
- Owner: read and write.
- Group: read only.
- Others: read only.
Also verify the ownership is appropriate:
ls -la /var/www/html/.htaccess
-rw-r--r-- 1 www-data www-data 245 Oct 15 14:30 .htaccess
If the file is owned by a different user, you may need to change ownership:
sudo chown www-data:www-data /var/www/html/.htaccess
Permission problems are particularly sneaky because Apache does not log an error when it cannot read an .htaccess file. It simply skips it. If your rules are not taking effect and the error log is silent, check permissions first.
Other Common Mistakes
Missing RewriteEngine On:
Every set of rewrite rules needs RewriteEngine On before any RewriteRule or RewriteCond. Without it, the rules are silently ignored:
# Wrong: missing RewriteEngine On
RewriteRule ^about$ /about.html [L]
# Correct
RewriteEngine On
RewriteRule ^about$ /about.html [L]
Leading slash in RewriteRule patterns:
In .htaccess context, the URL path passed to RewriteRule has the leading slash stripped. A pattern that starts with / will never match:
# Wrong: leading slash will never match in .htaccess
RewriteRule ^/about$ /about.html [L]
# Correct: no leading slash in the pattern
RewriteRule ^about$ /about.html [L]
Wrong order of directives:
As covered in the previous articles, directive order matters. Placing rewrites before redirects or access control before HTTPS enforcement can produce unexpected results. Refer to the recommended ordering from the Syntax and Directives article.
Rewrite Logging
When mod_rewrite rules are not behaving as expected, the error log alone may not give you enough information. Apache provides a dedicated rewrite log that traces exactly how each rule is evaluated.
Enabling Rewrite Logging
In Apache 2.4, rewrite logging is controlled through the LogLevel directive. This directive must be set in the main server configuration (it cannot be set in .htaccess):
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/html
# Enable detailed rewrite logging
LogLevel alert rewrite:trace3
</VirtualHost>
The rewrite:trace level ranges from trace1 (minimal detail) to trace8 (maximum detail). For most debugging scenarios, trace3 provides a good balance of useful information without overwhelming output:
| Level | Detail |
|---|---|
trace1 | Logs only the final result of rewrite processing. |
trace2 | Logs the result plus the rules that matched. |
trace3 | Logs conditions, rule matching, and substitutions. |
trace4 through trace8 | Increasingly verbose internal processing details. |
After adding the LogLevel directive, restart Apache:
sudo systemctl restart apache2
Reading Rewrite Log Output
With rewrite:trace3 enabled, the error log will contain entries like these for each request:
[rewrite:trace3] [pid 12345] mod_rewrite.c(477): [client 127.0.0.1:54321]
127.0.0.1 - - [example.com/sid#...] [perdir /var/www/html/]
strip per-dir prefix: /var/www/html/about -> about
[rewrite:trace3] [pid 12345] mod_rewrite.c(477): [client 127.0.0.1:54321]
127.0.0.1 - - [example.com/sid#...] [perdir /var/www/html/]
applying pattern '^about$' to uri 'about'
[rewrite:trace3] [pid 12345] mod_rewrite.c(477): [client 127.0.0.1:54321]
127.0.0.1 - - [example.com/sid#...] [perdir /var/www/html/]
rewrite 'about' -> '/about.html'
This trace shows you:
- Apache stripped the per-directory prefix (
/var/www/html/) from the URL, leavingabout. - The pattern
^about$was applied to the URIabout. - The rule matched and rewrote
aboutto/about.html.
If a rule does not match, the log will show that the pattern was tested but did not apply, helping you identify whether your regex pattern is correct.
A Debugging Walkthrough
Suppose you have this rule and it is not working:
RewriteEngine On
RewriteRule ^products/([0-9]+)$ /product.php?id=$1 [L]
You visit /products/42 but get a 404 error instead of the expected page. Enable rewrite:trace3, make the request again, and check the log:
applying pattern '^products/([0-9]+)$' to uri 'products/42'
rewrite 'products/42' -> '/product.php?id=42'
[perdir /var/www/html/] add per-dir prefix: /product.php -> /var/www/html/product.php
[perdir /var/www/html/] trying to replace prefix /var/www/html/ with /
internal redirect with /product.php?id=42 [INTERNAL REDIRECT]
The rewrite itself is working perfectly. The issue is that /var/www/html/product.php does not exist on disk. The rule is correct, but the target file is missing. Without the rewrite log, you might have spent a long time suspecting a regex problem.
Never leave rewrite tracing enabled in production. Even trace1 generates a significant amount of log output for every request, which can fill your disk and degrade performance. Enable it only for debugging, and set the level back to warn or error when you are done:
LogLevel warn
Then restart Apache:
sudo systemctl restart apache2
Staging vs. Production Workflow
A disciplined workflow for developing and deploying .htaccess changes prevents problems from reaching your live site.
The Recommended Workflow
Step 1: Develop locally.
Make all changes on your local Apache installation or a Docker container. Use AllowOverride All and enable rewrite logging so you can iterate quickly and see detailed feedback.
# Watch the error log while testing
sudo tail -f /var/log/apache2/error.log
Step 2: Test with curl.
Use curl to verify headers, redirects, and status codes programmatically. This is faster and more reliable than manually checking in a browser:
# Check response headers
curl -I http://localhost/test-page
# Follow redirects and show the chain
curl -LI http://localhost/old-page
# Check for a specific header
curl -sI http://localhost/ | grep -i "x-frame-options"
Step 3: Deploy to staging.
If you have a staging server that mirrors production, deploy your .htaccess changes there first. Test with real domain names, SSL certificates, and production-like traffic patterns. This catches issues that only appear in specific environments, like HTTPS redirect behavior behind a load balancer.
Step 4: Deploy to production.
Once verified on staging, deploy the .htaccess file to production. Monitor the error log closely for the first few minutes:
sudo tail -f /var/log/apache2/error.log
Step 5: Verify in production.
Run the same curl checks against your production domain:
curl -I https://www.example.com/
curl -LI http://example.com/old-page
Version Control
Treat your .htaccess file like any other source code file. Keep it in version control (Git, for example) so you can:
- Track every change with a descriptive commit message.
- Quickly revert to a previous working version if something breaks.
- Review changes before deploying through pull requests or code reviews.
git add .htaccess
git commit -m "Add HTTPS redirect and security headers"
If a deployment causes problems, reverting is immediate:
git checkout HEAD~1 -- .htaccess
Keep a Backup
Even if you use version control, keeping a quick backup before editing is a good habit, especially on shared hosting where Git may not be available:
cp .htaccess .htaccess.bak
If the new version causes a 500 error and you cannot access your site to debug it, you can restore the backup through your hosting control panel's file manager, FTP, or SSH:
cp .htaccess.bak .htaccess
A Pre-Deployment Checklist
Before deploying .htaccess changes to production, run through this checklist:
- Main config syntax is valid:
sudo apachectl configtestreturnsSyntax OK. - Local testing passes: All affected URLs return the expected status codes and headers.
- No infinite redirect loops: Test redirect rules with
curl -LIand confirm the chain terminates. - Required modules are enabled: Every
<IfModule>block references a module that is available in production. - File encoding is correct: UTF-8, no BOM, Unix line endings (LF).
- File permissions are correct:
644on the.htaccessfile. - Backup exists: A copy of the current working
.htaccessis saved. - Version control is updated: Changes are committed with a meaningful message.
Consider creating a simple shell script that runs your key curl checks automatically. You can run it after every deployment to quickly verify that headers, redirects, and status codes are all correct:
#!/bin/bash
echo "Checking HTTPS redirect..."
curl -sI http://example.com/ | grep "Location"
echo "Checking security headers..."
curl -sI https://example.com/ | grep -iE "x-frame|x-content-type|strict-transport"
echo "Checking 404 page..."
curl -sI https://example.com/nonexistent-page | grep "HTTP"
echo "Done."
Save this as check-htaccess.sh, make it executable with chmod +x check-htaccess.sh, and run it after each deployment.