Summary
When using inline purge (proxy_cache_purge in the same location as
proxy_pass), $proxy_host must not be used in proxy_cache_key. Doing so
causes every purge request to return 412 Precondition Failed, even
immediately after confirming the entry is cached ($upstream_cache_status: HIT).
This isn't a bug in the module itself — it's a consequence of how
$proxy_host is evaluated by ngx_http_proxy_module, combined with how
ngx_http_rewrite_module processes set/break — but it's an easy trap to
fall into, cost me a full day to track down, and the README doesn't mention
it anywhere (Directives / Sample configurations / Troubleshooting). Filing
this as a documentation suggestion, split into two related gotchas since the
first fix I tried for gotcha #1 ran straight into gotcha #2.
Gotcha #1: $proxy_host is not available during PURGE
$proxy_host is computed by ngx_http_proxy_eval() during the proxy_pass
phase of request processing. A PURGE request handled by this module never
reaches that phase — there's nothing to proxy — so $proxy_host resolves to
something different (empty, in my case) than it does during a normal GET.
The MD5 hash computed for the purge key therefore never matches the hash
stored in the cache file, and the lookup always misses -> 412, regardless
of whether the entry actually exists on disk.
Repro
proxy_cache_path /var/cache/nginx/test keys_zone=test:32m;
server {
location / {
proxy_cache test;
proxy_cache_key "$proxy_host$request_uri";
proxy_cache_valid 200 1d;
proxy_cache_purge PURGE from 127.0.0.1;
proxy_pass https://backend.example.com;
}
}
curl -sI https://example.com/foo.jpg # 200, MISS
curl -sI https://example.com/foo.jpg # 200, HIT (confirmed cached)
curl -X PURGE https://example.com/foo.jpg # always 412, even though HIT above
Cache file on disk confirms the entry exists, with $proxy_host correctly
resolved at store-time:
KEY: backend.example.com/foo.jpg
Gotcha #2: replacing it with set $backend_host ...; inside a location that has rewrite ... break silently does nothing
The obvious fix looks like:
location /content/ {
rewrite ^/[^/]+/(.*)$ /content/$1 break;
set $backend_host "backend.example.com"; # in an included file, after break
proxy_cache_key "$backend_host$request_uri";
proxy_pass https://backend.example.com;
}
This does not work either, and fails silently — no error at startup, no
error at runtime, $backend_host just resolves to an empty string. Resulting
cache key:
(no host prefix at all).
Reason: set, rewrite, if, return, and break all belong to
ngx_http_rewrite_module and are executed in file order, within a single
rewrite-phase pass for a given location. break stops that module's
processing for the rest of the phase. Since the set directive appeared
after the break in file order (it was inside an included file placed
below the rewrite ... break line), it was simply never reached/executed.
This is easy to get wrong because set/proxy_cache_key/proxy_pass all
look like static config directives evaluated top-to-bottom regardless of
control flow — but set is not; it's rewrite-module bytecode subject to the
same break/last short-circuiting as rewrite itself.
Fix that actually works
Move set to the server{} block, before any location that contains
a break:
server {
server_name example.com;
set $backend_host "backend.example.com"; # <-- here, at server level
location /content/ {
rewrite ^/[^/]+/(.*)$ /internal/$1 break;
proxy_cache_key "$backend_host$request_uri"; # now resolves correctly
proxy_pass https://backend.example.com;
}
}
Directives at server scope run before location-scope ones in the rewrite
phase, so the variable is already set before any break in a location has a
chance to short-circuit further processing.
Confirmed working, end to end
curl -k -sI https://example.com/foo.jpg # MISS
curl -k -sI https://example.com/foo.jpg # HIT
find /var/cache/nginx/test -type f -exec head -5 {} \;
# KEY: backend.example.com/foo.jpg <- correct, with host prefix
curl -k -i -X PURGE https://example.com/foo.jpg
# HTTP/2 200
# <html>...<p>Status: purged</p>...</html>
Suggestion
Add a short note under Troubleshooting in the README covering both
gotchas, something like:
Purge always returns 412 despite a confirmed cache HIT — check whether
proxy_cache_key uses $proxy_host. This variable is only evaluated
during the proxy_pass phase and is not available (or differs) during a
PURGE request, causing the computed hash to never match. Use $host, or
set an explicit variable instead — but see the next point.
set $var inside a location with rewrite ... break doesn't take
effect — set is a rewrite-module directive and is subject to the same
break/last short-circuiting as rewrite. If set appears after a
break in the same location (directly or via an included file), it
never executes and the variable stays empty. Declare it at server scope
instead, before any location-level break.
For reference, magiclen/nginx-cache-purge (a different tool solving the
same underlying problem) documents the $proxy_host limitation explicitly
in its README, so gotcha #1 at least seems to be a fairly common trap worth
calling out here too. Gotcha #2 seems specific enough to this
include-heavy config pattern that I haven't seen it documented anywhere.
Environment
- nginx: 1.30.4 (official nginx.org build,
--with-compat)
- Module:
ngx_http_cache_purge_module.so, loaded via load_module (dynamic module)
- OS: RHEL-family (built by gcc 8.5.0)
Summary
When using inline purge (
proxy_cache_purgein the samelocationasproxy_pass),$proxy_hostmust not be used inproxy_cache_key. Doing socauses every purge request to return
412 Precondition Failed, evenimmediately after confirming the entry is cached (
$upstream_cache_status: HIT).This isn't a bug in the module itself — it's a consequence of how
$proxy_hostis evaluated byngx_http_proxy_module, combined with howngx_http_rewrite_moduleprocessesset/break— but it's an easy trap tofall into, cost me a full day to track down, and the README doesn't mention
it anywhere (Directives / Sample configurations / Troubleshooting). Filing
this as a documentation suggestion, split into two related gotchas since the
first fix I tried for gotcha #1 ran straight into gotcha #2.
Gotcha #1:
$proxy_hostis not available during PURGE$proxy_hostis computed byngx_http_proxy_eval()during theproxy_passphase of request processing. A PURGE request handled by this module never
reaches that phase — there's nothing to proxy — so
$proxy_hostresolves tosomething different (empty, in my case) than it does during a normal GET.
The MD5 hash computed for the purge key therefore never matches the hash
stored in the cache file, and the lookup always misses ->
412, regardlessof whether the entry actually exists on disk.
Repro
Cache file on disk confirms the entry exists, with
$proxy_hostcorrectlyresolved at store-time:
Gotcha #2: replacing it with
set $backend_host ...;inside a location that hasrewrite ... breaksilently does nothingThe obvious fix looks like:
This does not work either, and fails silently — no error at startup, no
error at runtime,
$backend_hostjust resolves to an empty string. Resultingcache key:
(no host prefix at all).
Reason:
set,rewrite,if,return, andbreakall belong tongx_http_rewrite_moduleand are executed in file order, within a singlerewrite-phase pass for a given
location.breakstops that module'sprocessing for the rest of the phase. Since the
setdirective appearedafter the
breakin file order (it was inside anincluded file placedbelow the
rewrite ... breakline), it was simply never reached/executed.This is easy to get wrong because
set/proxy_cache_key/proxy_passalllook like static config directives evaluated top-to-bottom regardless of
control flow — but
setis not; it's rewrite-module bytecode subject to thesame
break/lastshort-circuiting asrewriteitself.Fix that actually works
Move
setto theserver{}block, before anylocationthat containsa
break:Directives at
serverscope run beforelocation-scope ones in the rewritephase, so the variable is already set before any
breakin a location has achance to short-circuit further processing.
Confirmed working, end to end
Suggestion
Add a short note under Troubleshooting in the README covering both
gotchas, something like:
For reference,
magiclen/nginx-cache-purge(a different tool solving thesame underlying problem) documents the
$proxy_hostlimitation explicitlyin its README, so gotcha #1 at least seems to be a fairly common trap worth
calling out here too. Gotcha #2 seems specific enough to this
include-heavy config pattern that I haven't seen it documented anywhere.Environment
--with-compat)ngx_http_cache_purge_module.so, loaded viaload_module(dynamic module)