HTENGIX
HTENGIX is a patched NGINX build that does the one thing stock NGINX famously refuses to do:
it reads .htaccess files from your docroot at request time, with Apache per-directory semantics.
Point it at an existing Apache document root and your mod_rewrite rules — WordPress pretty
permalinks, security-plugin rules, custom redirects — just work, without translating them
to nginx.conf syntax first.
Beta — testing channel
HTENGIX is in beta and ships exclusively through the getpagespeed-extras-testing
channel. The supported directive scope is deliberately narrow (see
what works today and what does not yet) and is
expanding release by release. Don't run it on production workloads that depend on
directives outside the supported scope.
What it does
With htaccess on; in your NGINX configuration, HTENGIX walks the filesystem path of each
request and applies .htaccess files the way Apache does:
- Per-directory rewrite semantics — patterns match the directory-relative path (no leading
slash), relative substitutions re-anchor at the directory or
RewriteBase, exactly like Apache's per-dirmod_rewrite. - Closest-wins merge order — the nearest
.htaccesswith rewrite configuration replaces ancestors' rewrite configuration, matching Apache's default (non-Inherit) behavior. <IfModule>blocks — including negation and nesting;mod_rewritecounts as loaded, and blocks for unemulated modules are skipped wholesale, like Apache.- Nonexistent paths — requests to not-yet-existing paths fall back to the deepest existing ancestor directory, so front-controller patterns (WordPress, Laravel, etc.) rewrite correctly.
.ht*request denial — direct requests to.htaccess/.htpasswdreturn 403, matching Apache's stockFilesMatch "^\.ht"protection.- Change pickup without reloads —
.htaccessedits take effect on the next request (mtime-based cache), nonginx -s reloadneeded.
What works today
The mod_rewrite directive category is implemented in full and parity-tested 1:1 against
Apache 2.4:
| Area | Coverage |
|---|---|
RewriteRule |
Full flag set: L, NC, QSA, QSD, QSL, R[=code], F, G, END, PT, P, C, N, B, NE, NS, DPI, E=, CO=, T=, H=, -, S=n |
RewriteCond |
All test operators: -f, -d, -e, -s, -l, -x, -U, -F, string and integer comparisons, ! negation, [OR]/[NC] flags, %{VARIABLE} server variables |
RewriteBase, RewriteEngine, RewriteOptions |
Supported (RewriteOptions Inherit from .htaccess is a tracked gap) |
<IfModule> containers |
Full, including nesting and ! negation |
What does not work yet
Honesty over hype: HTENGIX currently warns and skips these .htaccess directive
categories — they are parsed but not applied:
ErrorDocumentOptions(includingOptions -Indexes)Header(mod_headers)ExpiresActive/ExpiresByType/ExpiresDefault(mod_expires)Order/Allow/DenyandRequire(access control)- Containers other than
<IfModule>—<Files>,<FilesMatch>,<Limit>blocks are skipped whole (their contents are never mis-applied at top level) RewriteOptions Inheritdeclared inside.htaccess
Each skipped directive is logged, so you can audit exactly what a given .htaccess needs
beyond the supported scope. If your site's .htaccess relies on these, keep their NGINX
equivalents (error_page, add_header, expires, deny) in your server block — the
Apache to NGINX converter
generates them.
There are also two documented engine divergences from Apache 2.4, asserted by regression tests so they never change silently:
| URL shape | Apache 2.4 | HTENGIX |
|---|---|---|
/a%2Fb (encoded slash) |
404 (AllowEncodedSlashes off default) |
decodes; catch-all rules apply |
/index.php/extra (PATH_INFO on a PHP-less baseline) |
404 | catch-all rules apply |
How we test parity
"Parity-tested" is a concrete, reproducible claim, not a slogan:
- A differential harness boots real Apache httpd 2.4 and HTENGIX side by side over
the same document root and the same unmodified
.htaccessfiles. - Both servers replay an identical set of request tuples (method, URI, headers), and the
harness diffs the full outcome: status code, redirect
Location, and the internally rewritten target. - The fixtures are real-world: the canonical WordPress
.htaccess(26 request tuples) and an anonymized real customer site running WordPress with Sucuri WAF rules (15 tuples). All 41/41 tuples match Apache 2.4. - Every parity run is codified as a self-contained golden test in the module's CI, so any regression fails the build.
- The result was re-verified end-to-end from the published RPM on a clean Rocky Linux 9 container — not just from a development build.
How to install HTENGIX
HTENGIX is available for RHEL 9 / Rocky Linux 9 / AlmaLinux 9 (x86_64) from the
getpagespeed-extras-testing channel, which requires an active
repository subscription.
More distributions will follow as the beta progresses.
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install dnf-plugins-core
dnf config-manager --set-enabled getpagespeed-extras-testing
dnf -y install htengix
systemctl enable --now nginx
HTENGIX provides nginx, so it slots in as a drop-in package replacement while
preserving your existing configuration and installed modules:
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install dnf-plugins-core
dnf config-manager --set-enabled getpagespeed-extras-testing
dnf swap nginx htengix
service nginx upgrade
Then enable .htaccess processing for a server (or location) that serves an Apache-style
docroot:
server {
listen 80;
server_name example.com;
root /var/www/example.com;
htaccess on;
index index.php index.html;
# ... your usual PHP-FPM / static config ...
}
Watch the error log on first requests: any .htaccess directive outside the supported scope
is reported there, giving you a precise migration checklist.
How to switch back to stable NGINX
dnf config-manager --set-disabled getpagespeed-extras-testing
dnf swap htengix nginx
Your NGINX configuration is untouched by the swap; only the htaccess directive needs
removing.
Feedback
HTENGIX is built in the open against real-world .htaccess corpora. If a rule from your
site doesn't behave like it does under Apache, that's exactly the report we want —
contact us with the .htaccess
snippet and the request URL.