The Mystery of mod_rewrite

I collected a bit of information from mod_rewrite "fun" over the years at stackoverflow, but then figured I'd post it here too, since I've been a bit lax about writing. Don't worry though (because I know you are), I have a few things in the pipes, including the next article in our MVC bare bones framework series, and my take on a drupal based image gallery.

On a completely unrelated note, the macbook (version 2) is finally here! I'm not going to touch it, because I don't want to be blamed for anything that goes wrong this time around. On the plus side, Apple support very generously gave the SO a $30 off coupon, for purchases over $200 for her troubles. (I'm not sure if that last sentence was sarcastic or not...)

And now, mod_rewrite fun:

Where to place mod_rewrite rules

mod_rewrite rules may be placed within the httpd.conf file, or within the .htaccess file. If you have access to httpd.conf, placing rules here will offer a performance benefit (as the rules are processed once, as opposed to each time the .htaccess file is called).

Logging mod_rewrite requests

Logging may be enabled from within the httpd.conf file (including ):

# logs can't be enabled from .htaccess
# loglevel > 2 is really spammy!
RewriteLog /path/to/rewrite.log
RewriteLogLevel 2

Common use cases

  • To funnel all requests to a single point:
    RewriteEngine on
    RewriteCond %{REQUEST_FILENAME} !-f   # ignore existing files
    RewriteCond %{REQUEST_FILENAME} !-d   # ignore existing directories
    RewriteRule ^(.*)$ index.php?query=$1 # map requests to index.php and
                                          # append as a query string
    
  • Handling 301/302 redirects:
    RewriteEngine on
    RewriteRule ^oldpage.html$ /newpage.html [R=302]  # 302 Redirect
    RewriteRule ^oldpage2.html$ /newpage.html [R=301] # 301 Redirect
    

    note: external redirects are implicitly 301 redirects:

    # this rule:
    RewriteRule ^somepage.html$ http://google.com
    # is equivalent to:
    RewriteRule ^somepage.html$ http://google.com [R]
    # and:
    RewriteRule ^somepage.html$ http://google.com [R=301]
    
  • Forcing SSL
    RewriteEngine on
    RewriteCond %{SERVER_PORT} 80
    RewriteRule ^(.*)$ https://mysite.com/$1 [R,L]
    
  • Common flag usage:
    • [R] force a redirect (default 301)
    • [R=302] force a 302 redirect
    • [L] stop rewriting process (see note below in common pitfalls)
    • [NC] case insensitive matches

    You can mix and match flags:

    RewriteRule ^olddir(.*)$ /newdir$1 [L,NC]
    

Common pitfalls

  • Mixing mod_alias style redirects with mod_rewrite
    # Bad
    Redirect 302 /somepage.html http://mysite.com/otherpage.html
    RewriteEngine on
    RewriteRule ^(.*)$ index.php?query=$1
    
    # Good (use mod_rewrite for both)
    RewriteEngine on
    RewriteRule ^somepage.html$ /otherpage.html [R=302] # 302 redirect
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.*)$ index.php?query=$1 # handle other redirects
    

    note: you can mix mod_alias with mod_rewrite, but it involves more work than just handling basic redirects as above.

  • Context affects syntax
    # given: GET /directory/file.html
    

    Within .htaccess files, a leading slash is not used in the pattern:

    # .htaccess
    # result: /newdirectory/file.html
    RewriteRule ^directory(.*)$ /newdirectory$1
    
    # .htaccess
    # result: no match!
    RewriteRule ^/directory(.*)$ /newdirectory$1
    
    # httpd.conf
    # result: /newdirectory/file.html
    RewriteRule ^/directory(.*)$ /newdirectory$1
    
  • [L] is not last! (sometimes)
    RewriteRule ^dirA$ /dirB [L] # processing does not stop here
    RewriteRule ^dirB$ /dirC     # /dirC will be the final result
    

    Within the .htaccess context, [L] will not force mod_rewrite to stop. it will continue to trigger internal sub-requests. Our rewrite log reveals the details:

    rewrite 'dirA' -> '/dirB'
    internal redirect with /dirB [INTERNAL REDIRECT]
    rewrite 'dirB' -> '/dirC'