yum upgrades for production use, this is the repository for you.
Active subscription is required.
Technical Briefing: Rendering XML as HTML at the Edge with NGINX’s XSLT Filter Module
Today’s source is a GetPageSpeed article on ngx_http_xslt_filter_module, an established NGINX module rather than a new release. It applies XSLT stylesheets at the web-server layer, so a backend can keep returning XML while NGINX returns HTML to clients. The article covers how the filter works, installation, all six directives, practical configurations, testing, performance, security, troubleshooting, and when the module is the right tool.
The Problem: Raw XML in Front of Users
Some data sources only speak XML:
- legacy SOAP services
- third-party RSS feeds
- old internal systems
Served directly, these show users raw angle brackets instead of a readable page. You can fix this in application code, but then presentation logic lives in the backend. Every XML schema change or new endpoint requires a code change and a redeploy.
The module is most useful when you can’t modify the XML source, such as vendor feeds, third-party APIs, or legacy services nobody can rebuild. It also processes XML with awareness of its structure. That goes beyond the plain text replacement offered by the substitutions filter module.
How the Filter Works
The module processes a response in four stages:
- Header filter. It checks two things: the response’s content type matches
xslt_types(defaulttext/xml), and a stylesheet is configured. - Body filter. It feeds body chunks to an incremental libxml2 parser, then validates the complete document.
- Transformation. It applies stylesheets in order using libxslt. With multiple
xslt_stylesheetdirectives, each stylesheet’s output becomes the next one’s input. - Output. It sends the result with updated
Content-TypeandContent-Length. The content type comes from the stylesheet’sxsl:outputelement.
Caveat: The module buffers the entire response body in memory. For large proxied XML, you may need to tune proxy_buffer_size.
Installation
RHEL / CentOS / AlmaLinux / Rocky
These commands use a third-party prebuilt module repository:
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-xslt
Then add this line at the top of /etc/nginx/nginx.conf, before any other blocks:
load_module modules/ngx_http_xslt_filter_module.so;
Debian / Ubuntu
After setting up the APT repo, run:
sudo apt-get update
sudo apt-get install nginx-module-xslt
The module loads automatically, so no load_module line is needed.
Verify
Add an XSLT directive to your configuration and run nginx -t.
The Six Directives
| Directive | Context / Default | Behavior |
|---|---|---|
xslt_stylesheet file [param=value ...] |
location | Can be repeated to build a pipeline. Inline params may include NGINX variables. |
xslt_param name value |
http/server/location | Value is evaluated as an XPath expression after NGINX variable expansion. |
xslt_string_param name value |
http/server/location | Value is always a plain string. This is the safer choice for variables and literals. |
xslt_types mime-type ... |
default text/xml |
Sets which response types are transformed. |
xslt_last_modified on \| off |
default off |
Off: Last-Modified and ETag are stripped. On: the original Last-Modified is kept and a weak ETag is generated. |
xml_entities file |
— | Points to a DTD for custom entities beyond & < > " '. |
Some directives need care:
- With
xslt_param, literal strings need inner quoting:
nginx
xslt_param name "'static-value'"; xslt_string_paramneeds no extra quoting, which makes it well suited to variables:
nginx
xslt_string_param currency $arg_currency;xslt_typescan list several types:
nginx
xslt_types text/xml application/xml application/rss+xml;
Practical Configurations
Static XML catalog
location = /catalog {
default_type text/xml;
xslt_stylesheet /etc/nginx/xslt/catalog.xsl;
alias /var/www/data/products.xml;
}
default_type text/xml is required here. Without it, the file gets a generic MIME type and the filter never fires.
RSS feed to HTML
This uses the same pattern as the catalog, with the rss2html.xsl stylesheet applied to /var/www/data/feed.xml.
Upstream XML API
Combine a proxy with a stylesheet:
proxy_pass http://backend-api/api/products;
If the upstream sends application/xml rather than text/xml, add:
xslt_types text/xml application/xml;
Query-driven filtering
xslt_string_param title "Our Premium Products";
xslt_string_param currency_filter $arg_currency;
With this configuration:
– /catalog?currency=USD shows only USD items.
– /catalog shows all items.
Chained pipeline
Put normalize.xsl followed by render.xsl in the same location. The output of the first stylesheet becomes the input of the second.
Testing
- Run
nginx -t. The errorunknown directive "xslt_stylesheet"means the module isn’t loaded. - Reload NGINX:
bash
sudo systemctl reload nginx - Check the response:
bash
curl -D- http://localhost/catalog
ExpectContent-Type: text/html; charset=UTF-8and an HTML body.
If you still get raw XML, check for:
– a content type that doesn’t match xslt_types
– a wrong or unreadable stylesheet path
– malformed XML
Performance
- Memory. Full-body buffering can be costly for documents of 10+ MB. Split them or transform them offline.
- CPU. Transformation is CPU-heavy, so cache the output for high-traffic endpoints:
nginx
proxy_cache_path /var/cache/nginx/xslt levels=1:2 keys_zone=xslt_cache:10m max_size=100m;
Then enable the cache in the location:
nginx
proxy_cache xslt_cache;
proxy_cache_valid 200 5m; - Stylesheets and DTDs. These are compiled or loaded once, at config load or startup, so there is no per-request parsing cost.
- Conditional requests.
xslt_last_modified onenablesIf-Modified-Sincehandling, which reduces re-transformation.
Security
- Stylesheet location. Keep stylesheets outside the web root. Use a path like
/etc/nginx/xslt/, not/var/www/html/templates/. - Permissions.
bash
chmod 640 /etc/nginx/xslt/*.xsl
chown root:nginx /etc/nginx/xslt/*.xsl - Malformed XML. Malformed XML returns HTTP 500, which a misbehaving upstream can trigger. Mitigate with:
nginx
proxy_intercept_errors on;
error_page 500 /fallback.html; - Stylesheet paths. Never build them from user input. This is dangerous:
nginx
xslt_stylesheet /var/www/templates/$arg_template;
Always use fixed paths. - User-derived values. Use
xslt_string_paramrather thanxslt_paramto avoid unexpected XPath evaluation.
Troubleshooting
- No transformation. Likely causes:
- a type mismatch
- a missing
xslt_stylesheet - a static file without a
.xmlextension served viaaliasortry_files(adddefault_type text/xml;)
- HTTP 500. Watch the error log:
bash
tail -f /var/log/nginx/error.log
Look forxsltApplyStylesheet()failures or parser errors. Likely causes are invalid upstream XML, stylesheet references to nonexistent nodes, or an invalid XPath inxslt_param. - Wrong encoding. Always set the encoding explicitly, because otherwise the default depends on the output method:
xml
<xsl:output method="html" encoding="UTF-8"/>
The Payoff: When to Use It
Good fit:
– XML APIs at the proxy layer
– RSS/Atom rendering
– modernizing the presentation of legacy SOAP data
– one shared transformation across multiple upstreams
Use the substitutions filter instead when you only need simple string replacement.
Skip it for new applications that control both backend and frontend. Emitting HTML or JSON directly is simpler there.
The main benefit is that presentation moves out of backend code and into NGINX configuration and stylesheets. XML sources you can’t or won’t change still produce readable HTML. Caching and conditional requests offset the CPU cost, and the security practices above keep stylesheet handling locked down.
Read the full article: NGINX XSLT Module: Transform XML Responses into HTML
