8. RewriteMap¶
And memories, he knew, were not glass treasures to bekept locked within a box. They were bright ribbons tobe hung in the wind.—Terry Brooks, The Talismans of Shannara
The RewriteMap directive gives you a way to call external mapping
routines to simplify a RewriteRule. This external mapping can be a
flat text file containing one-to-one mappings, or a database, or a
script that produces mapping rules, or a variety of other similar
things. In this chapter we’ll discuss how to use a RewriteMap in a
RewriteRule or RewriteCond.
8.1. Creating a RewriteMap¶
The RewriteMap directive creates an alias which you can then invoke in
either a RewriteRule or RewriteCond directive. You can think of it
as defining a function that you can call later on.
The syntax of the RewriteMap directive is as follows:
RewriteMap MapName MapType:MapSource
Where the various parts of that syntax are defined as:
- MapName
The name of the ‘function’ that you’re creating
- MapType
The type of the map. The various available map types are discussed below.
- MapSource
The location from which the map definition will be obtained, such as a file, database query, or predefined function.
The RewriteMap directive must be used either in virtualhost context,
or in global server context. This is because a RewriteMap is loaded at
server startup time, rather than at request time, and, as such, cannot
be specified in a .htaccess file.
8.2. Using a RewriteMap¶
Once you have defined a RewriteMap, you can then use it in a
RewriteRule or RewriteCond as follows:
RewriteMap examplemap txt:/path/to/file/map.txt
RewriteRule ^/ex/(.*) ${examplemap:$1}
Note in this example that the RewriteMap, named ‘examplemap’, is
passed an argument, $1, which is captured by the RewriteRule
pattern. It can also be passed an argument of another known variable.
For example, if you wanted to invoke the examplemap map on the entire
requested URI, you could use the variable %{REQUEST_URI} rather than
$1 in your invocation:
RewriteRule ^ ${examplemap:%{REQUEST_URI}}
8.3. Default Values¶
When a key is not found in the map, the lookup returns an empty string
by default. You can specify a fallback using the pipe character (|)
followed by a default value:
${mapname:key|default}
For example:
RewriteRule ^/product/(.*) /prods.php?id=${productmap:$1|NOTFOUND} [PT]
If the key $1 is not found in productmap, the value
NOTFOUND is substituted instead. This lets your application handle
the missing-key case gracefully rather than receiving an empty string.
The default value can be any string, including a URL path:
RewriteRule ^/old/(.*) ${redirectmap:$1|/not-found.html} [R=301]
8.4. RewriteMap Types¶
There are a number of different map types which may be used in a
RewriteMap.
8.4.1. int¶
An int map type is an internal function, pre-defined by mod_rewrite
itself. There are four such functions:
8.4.2. toupper¶
The toupper internal function converts the provided argument text to
all upper case characters.
# Convert any lower-case request to upper case and redirect
RewriteMap uc int:toupper
RewriteRule (.*?[a-z]+.*) ${uc:$1} [R=301]
8.4.3. tolower¶
The tolower is the opposite of toupper, converting any argument text
to lower case characters.
# Convert any upper-case request to lower case and redirect
RewriteMap lc int:tolower
RewriteRule (.*?[A-Z]+.*) ${lc:$1} [R=301]
8.4.4. escape¶
The escape internal function URL-encodes special characters in the
argument, translating them to %xx hex sequences. This is useful
when a captured backreference might contain characters that would break
a URL — spaces, ampersands, question marks, and so on.
RewriteMap esc int:escape
RewriteRule ^/search/(.*)$ /search.php?term=${esc:$1} [PT]
A request for /search/x & y would result in the query string
term=x%20%26%20y, which is properly encoded for use in a URL.
This is similar to what the [B] flag does to backreferences, but
escape can be applied selectively to specific parts of the
substitution via the map syntax, whereas [B] affects all
backreferences in the rule.
8.4.5. unescape¶
The unescape internal function is the reverse of escape — it
decodes %xx hex sequences back to their original characters.
RewriteMap unesc int:unescape
RewriteCond ${unesc:%{QUERY_STRING}} (.*secret.*)
RewriteRule ^ - [F]
This example decodes the query string before testing it, so that
%73ecret is recognized as secret even when a client tries to
sneak it past a filter using percent-encoding.
Use unescape when you need to inspect or match the decoded form of
a URL component that may arrive in encoded form.
8.4.6. txt¶
A txt map is a plain text file containing one key-value pair per
line, separated by whitespace. Lines starting with # are comments.
The file format looks like this:
##
## productmap.txt - Product name to ID mapping
##
television 993
stereo 198
fishingrod 043
basketball 418
telephone 328
Define the map and use it in a rule:
RewriteMap productmap txt:/etc/httpd/maps/productmap.txt
RewriteRule ^/product/(.*) /prods.php?id=${productmap:$1|NOTFOUND} [PT]
A request for /product/television is internally rewritten to
/prods.php?id=993. If the product isn’t found in the map, the
default value NOTFOUND is used instead (see Default Values).
Caching: httpd caches the contents of a txt map in memory. The
cache is automatically refreshed when the file’s modification time
(mtime) changes, so you can update the file while the server is
running — no restart required. However, for very large map files
(thousands of entries), consider using a dbm map instead for
faster lookups.
Context restriction: The RewriteMap directive itself must
appear in server or virtual host context — you cannot declare it in a
.htaccess file. However, once declared, the map can be used in
RewriteRule and RewriteCond directives anywhere, including
.htaccess.
8.4.7. rnd¶
A rnd map uses the same text file format as txt, but the value
for each key is a pipe-separated list of alternatives. On each lookup,
one value is chosen at random.
##
## servers.txt - Backend server map for load balancing
##
static www1.example.com|www2.example.com|www3.example.com|www4.example.com
dynamic app1.example.com|app2.example.com
Define the map and use it with the [P] flag for proxy
load-balancing:
RewriteMap servers rnd:/etc/httpd/maps/servers.txt
RewriteRule ^/static/(.*) http://${servers:static}/$1 [P]
RewriteRule ^/app/(.*) http://${servers:dynamic}/app/$1 [P]
Each request for /static/logo.png is proxied to a randomly
selected server from the static list.
Weighting trick: To weight the selection toward a particular server, list it more than once:
static www1|www1|www1|www2
This gives www1 a 75% probability and www2 a 25% probability.
Note that this is a very basic form of load balancing with no health
checking or session affinity. For production load balancing, use
mod_proxy_balancer instead. The rnd map is most useful for
simple cases like distributing static asset requests or A/B testing.
8.4.8. dbm¶
A dbm map stores the same key-value data as a txt map but in a
DBM hash file, which provides O(1) lookups instead of a linear scan.
This matters when your map file has thousands of entries — a txt
map is read sequentially, while a dbm lookup is essentially
instantaneous regardless of file size.
Create a DBM file from a text file using the httxt2dbm utility
that ships with httpd:
httxt2dbm -i redirectmap.txt -o redirectmap.map
Then reference it in your configuration:
RewriteMap redirects dbm:/etc/httpd/maps/redirectmap.map
RewriteRule ^/old/(.*) ${redirects:$1|/gone.html} [R=301]
You can optionally specify the DBM type:
RewriteMap redirects dbm=sdbm:/etc/httpd/maps/redirectmap.map
Available types include sdbm (always available), gdbm,
ndbm, and db. In practice, use whatever httxt2dbm
produces by default — it chooses the best available type for your
platform.
Note: Some DBM implementations create two files (e.g.,
redirectmap.map.dir and redirectmap.map.pag). Always reference
the base name without extensions in the RewriteMap directive.
Caching: Like txt maps, dbm maps are cached in memory and
automatically refreshed when the file’s modification time changes. To
update a dbm map, regenerate it with httxt2dbm and httpd will
pick up the new version on the next lookup.
8.4.9. prg¶
A prg map launches an external program at server startup and
communicates with it via standard input and output. For each lookup,
the key is written to the program’s STDIN (followed by a newline), and
the program writes the result to STDOUT (also followed by a newline).
To indicate that a key has no match, the program should return the
string NULL (case-insensitive).
RewriteMap dash2under prg:/usr/local/bin/dash2under.py
RewriteRule - ${dash2under:%{REQUEST_URI}}
Here is an example program that replaces dashes with underscores:
#!/usr/bin/env python3
import sys
def main():
for line in sys.stdin:
key = line.strip()
result = key.replace('-', '_')
print(result, flush=True)
if __name__ == '__main__':
main()
Warning
External program maps come with serious caveats:
Flush your output. The program must flush STDOUT after every response line. Buffered output will cause httpd to hang waiting for a reply. In Python, use
flush=Trueonprint()or setPYTHONUNBUFFERED=1.Single-process bottleneck. Only one instance of the program runs. All requests that trigger the map are serialized through it. If the program is slow, it becomes a bottleneck for the entire server.
Hangs are fatal. If the program blocks without responding, httpd will hang waiting for it. There is no timeout.
Crashes are permanent. If the program dies, all subsequent lookups fail. You must restart httpd to relaunch it.
Startup only. The program is launched when httpd starts (or restarts). It is not re-launched on failure.
You can run the program as a specific user and group by adding a third argument:
RewriteMap mymap prg:/path/to/program user:group
For most use cases, a txt or dbm map — or even dbd — is
a better choice. Use prg only when you need logic that cannot be
expressed as a static lookup table.
8.4.10. dbd¶
A dbd map looks up keys via a SQL query, using a database
connection managed by mod_dbd. This lets you drive rewrite rules
from a database table that can be updated in real time without touching
configuration files or restarting the server.
There are two variants:
dbdExecutes the query on every lookup. Always returns the freshest data but incurs a database round-trip per request.
fastdbdCaches results in memory after the first lookup. Faster, but cached entries are not refreshed until httpd is restarted. Use this when the data changes infrequently.
The MapSource is a SQL SELECT statement with %s as a
placeholder for the lookup key:
RewriteMap myquery "fastdbd:SELECT destination FROM redirects WHERE source = %s"
RewriteRule ^/r/(.*) ${myquery:$1|/not-found.html} [R=301]
You must also configure mod_dbd with a database connection:
DBDriver pgsql
DBDParams "host=dbhost dbname=mydb user=httpd password=secret"
DBDMin 4
DBDKeep 8
DBDMax 20
DBDExptime 300
If the query returns multiple rows, one is selected at random —
similar to how rnd maps work. If no rows are returned, the
default value (after |) is used.
When to use dbd vs fastdbd: Use dbd when the underlying data
changes frequently and freshness matters (e.g., a vanity URL shortener
updated by a CMS). Use fastdbd when the data is relatively stable
and you want to minimize database load (e.g., a redirect table that’s
updated weekly).
mod_dbd is required — the dbd and fastdbd map types will
produce a configuration error if it is not loaded.