Skip to content

Docs: warn against $proxy_host in proxy_cache_key (inline purge always returns 412) #68

Description

@rapcore2

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:

KEY: /content/foo.jpg

(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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions