RewriteMap

RewriteMap and External Lookups

A map turns a key into a value inside a substitution or condition: ${name:key|default}. Maps are declared only in the server or virtual-host configuration, but .htaccess may use them. Types: txt (key-value lines, cached until the file changes), rnd (a random pick), dbm (a hash file from httxt2dbm, for thousands of keys), int (tolower, toupper, escape, unescape), prg (a long-running program) and dbd/fastdbd (SQL through mod_dbd, fastdbd cached).

/etc/apache2/maps/product-ids.php: a key per line in, an answer or NULL outApache config
#!/usr/bin/php
<?php
$ids = ['blue-mug' => 42, 'teapot' => 57];     // in real life, a database
while (($key = fgets(STDIN)) !== false) {
    fwrite(STDOUT, ($ids[trim($key)] ?? 'NULL') . "\n");
}
Three maps in the virtual host, then used from .htaccessApache config
# virtual host
RewriteEngine On
RewriteMap legacy "txt:/etc/apache2/maps/legacy.txt"
RewriteMap lc     "int:tolower"
RewriteMap ids    "prg:/etc/apache2/maps/product-ids.php" www-data:www-data
# .htaccess
RewriteEngine On
RewriteRule ^old/(.+)$ ${legacy:$1|/offers/} [R=301,L]
RewriteCond %{REQUEST_URI} [A-Z]
RewriteRule ^(.*)$ /${lc:$1} [R=301,L]
RewriteRule ^p/([a-z0-9-]+)$ show.php?id=${ids:$1|0} [L]

legacy.txt holds lines such as summer-sale-2025 /offers/summer:

Output of 47
$ t /old/summer-sale-2025 /old/gone /Shop/TEAPOT /p/teapot /p/unknown
/old/summer-sale-2025 301 http://example.com/offers/summer
/old/gone             301 http://example.com/offers/
/Shop/TEAPOT          301 http://example.com/shop/teapot
/p/teapot             200  {"id":"57"}
/p/unknown            200  {"id":"0"}

The default after | caught the missing text entry and the program's NULL. Apache 129 starts a prg program once and feeds it every lookup under the rewrite-map mutex, so it must flush each answer (PHP's fwrite to STDOUT does) and never stall. On 2.4.66 it did not start until the virtual host itself had RewriteEngine On; until then, every lookup silently returned the default. The third argument runs it as www-data, not root. One process serializes all lookups; prefer dbm for big tables and dbd for live data.