Debugging Apache CORS Header Issues: Why mod_headers Loads But Doesn’t Set Headers in .htaccess


2 views

Here's what I observed in my development environment (Apache 2.2.29 on macOS):

HTTP/1.1 200 OK
Server: Apache/2.2.29 (Unix)
Content-Type: application/json; charset=utf-8
# Missing all my CORS headers here!

First, let's confirm the essentials are in place:

# Check if module is loaded
httpd -M | grep headers_module
# Should return: headers_module (shared)

# Verify .htaccess permissions
<Directory "/path/to/your/project">
    AllowOverride All
    # ... other directives
</Directory>

After hours of debugging, I discovered that the module execution order was interfering with my headers. Here's what fixed it:

# In httpd.conf or virtual host config
<IfModule mod_headers.c>
    # Must come BEFORE mod_rewrite
    Header set Access-Control-Allow-Origin "*"
    Header set Access-Control-Allow-Methods "POST, GET, PUT, OPTIONS, PATCH, DELETE"
    Header set Access-Control-Allow-Headers "X-Accept-Charset,X-Accept,Content-Type"
</IfModule>

<IfModule mod_rewrite.c>
    RewriteEngine On
    # ... your rewrite rules
</IfModule>

When basic checks don't work, try these advanced methods:

# 1. Check for conflicting directives
apachectl -S
apachectl configtest

# 2. Enable trace logging
LogLevel debug
TraceEnable On

# 3. Test with minimal configuration
# Create a test .htaccess with ONLY headers:
Header set X-Test-Header "HelloWorld"

Watch out for these scenarios where headers might not apply:

  • When serving static files (AddType directives may interfere)
  • With certain MIME types (especially application/json)
  • When using PHP's header() function that overrides Apache headers
# Example workaround for JSON responses
<FilesMatch "\.(json)$">
    Header set Access-Control-Allow-Origin "*"
    Header set Access-Control-Allow-Methods "GET, POST, OPTIONS"
</FilesMatch>

Here's the bulletproof configuration that worked across all my environments:

# In .htaccess
<IfModule mod_headers.c>
    # CORS for API requests
    SetEnvIf Origin "^(.+)$" cors=$1
    Header set Access-Control-Allow-Origin "%{cors}e" env=cors
    Header merge Vary "Origin"
    
    # Preflight requests
    RewriteCond %{REQUEST_METHOD} OPTIONS
    RewriteRule ^(.*)$ $1 [R=200,L]
    
    Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
    Header set Access-Control-Allow-Headers "Content-Type, Authorization"
    Header set Access-Control-Allow-Credentials "true"
    Header set Access-Control-Max-Age "1728000"
</IfModule>

After implementing this, my headers appeared consistently in all responses:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
# ... other headers

After setting up Apache 2.2.29 on my Mac development environment, I encountered an odd issue where CORS headers defined in my .htaccess file weren't being applied, even though the headers_module was clearly loaded. Here's what my configuration looked like:


    Header set Access-Control-Allow-Origin "*"
    Header set Access-Control-Allow-Methods "POST, GET, PUT, OPTIONS, PATCH, DELETE" 
    Header set Access-Control-Allow-Headers "X-Accept-Charset,X-Accept,Content-Type"

First, I confirmed the headers module was indeed loaded:

$ httpd -M | grep headers
headers_module (shared)

And the module file existed in the specified location:

$ ls -la /usr/libexec/apache2/mod_headers.so
-rwxr-xr-x  1 root  wheel  38952 Dec 15  2014 /usr/libexec/apache2/mod_headers.so

The virtual host configuration appeared correct with proper AllowOverride settings:


    Options Indexes FollowSymLinks MultiViews
    AllowOverride All
    Require all granted

After extensive testing, I found the issue was related to Apache's processing order. The headers weren't being applied because:

  • The directives were being processed after PHP had already set headers
  • There was a conflict with mod_rewrite rules in the same .htaccess

Here's the modified .htaccess that finally worked:


    # Set headers early in the processing pipeline
    Header always set Access-Control-Allow-Origin "*"
    Header always set Access-Control-Allow-Methods "POST, GET, PUT, OPTIONS, PATCH, DELETE"
    Header always set Access-Control-Allow-Headers "X-Accept-Charset,X-Accept,Content-Type"
    
    # For preflight requests
    RewriteCond %{REQUEST_METHOD} OPTIONS
    RewriteRule ^(.*)$ $1 [R=200,L]



    RewriteEngine On
    RewriteBase /cms/public
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_URI} !^/(favicon\.ico|apple-touch-icon.*\.png)$ [NC]
    RewriteRule (.+) index.php?p=$1 [QSA,L]

The key changes were:

  1. Using Header always set instead of just Header set
  2. Adding specific handling for OPTIONS requests
  3. Ensuring headers are set before rewrite rules execute

After implementing these changes, the response headers now correctly include:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, GET, PUT, OPTIONS, PATCH, DELETE
Access-Control-Allow-Headers: X-Accept-Charset,X-Accept,Content-Type

If you're still facing issues, consider:

# Check if headers are being unset elsewhere
grep -r "Header unset" /etc/apache2/

# Test with minimal configuration
# Create test file headers-test.conf:

    Header set Test-Header "Works"