`caddyserver/cache-handler` doesn't purge by surrogate key

1. The problem I’m having:

I’m trying to purge the cache by tag / surrogate key. Caddy logs say the purge is happening, but in reality the key has not been purged.

2. Error messages and/or full log output:

I sent a request to https://localhost/souin-api/souin/surrogate_keys with header Surrogate-Key: localhost-dev and it was cached correctly. When I checked the /surrogate_keys API endpoint, I saw the key was there:

> curl -k https://localhost/souin-api/souin/surrogate_keys
{"localhost-dev":",GET-https-localhost-%2Ftest-path-123"}

I tried purging the localhost-dev surrogate key by sending PURGE to https://localhost/souin-api/souin with header Surrogate-Key: localhost-dev:

> curl -k -X PURGE https://localhost/souin-api/souin -H "Surrogate-Key: localhost-dev" -w "\nHTTP Response Code: %{http_code}\n" -s -o /dev/null

HTTP Response Code: 204

I get the response code 204 which is expected.

Here are the Caddy logs that are logged immediately when I send the PURGE request. You can see the incoming request is logged, and then cache handler logs the purge of localhost-dev surrogate key and all its belonging tags:

{"level":"debug","ts":"2025-04-23T14:59:31.633Z","logger":"http.handlers.cache","msg":"Incomming request &{Method:PURGE URL:/souin-api/souin Proto:HTTP/2.0 ProtoMajor:2 ProtoMinor:0 Header:map[Accept:[*/*] Surrogate-Key:[localhost-dev] User-Agent:[Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)]] Body:0x40001b1d40 GetBody:<nil> ContentLength:0 TransferEncoding:[] Close:false Host:localhost Form:map[] PostForm:map[] MultipartForm:<nil> Trailer:map[] RemoteAddr:172.17.0.1:62136 RequestURI:/souin-api/souin TLS:0x400038a9c0 Cancel:<nil> Response:<nil> Pattern: ctx:0x400088b710 pat:<nil> matches:[] otherValues:map[]}"}

{"level":"debug","ts":"2025-04-23T14:16:21.742Z","logger":"http.handlers.cache","msg":"Purge the tag localhost-dev"}

{"level":"debug","ts":"2025-04-23T14:16:21.742Z","logger":"http.handlers.cache","msg":"Purge the following tags: [ GET-https-localhost-%2Ftest-path-123]"}

But when I check, I can see the surrogate key is still there:

> curl -k https://localhost/souin-api/souin/surrogate_keys
{"localhost-dev":",GET-https-localhost-%2Ftest-path-123"}

If I send a GET request to https://localhost/test-path-123, I can see from the Cache-Status header that we got the cache hit instead of a cache miss:

Cache-Status: my_cache; hit; ttl=296; key=GET-https-localhost-/test-path-123; detail=REDIS

But if I use the /souin-api/souin/flush endpoint, it works and I can flush the entire cache. This is not what I want and I cannot use this. I’m mentioning it just in case it helps anyone debug this issue.

3. Caddy version:

2.10.0

4. How I installed and ran Caddy:

a. System environment:

Docker (Linux)

b. Command:

caddy run --config /etc/caddy/caddy.json

d. My complete Caddy config:

{
  "apps": {
    "cache": {
      "api": {
        "basepath": "/souin-api",
        "souin": {
          "basepath": "/souin",
          "enable": true,
          "security": false
        }
      },
      "log_level": "debug",
      "ttl": "5m",
      "headers": [
        "Content-Type",
        "Authorization"
      ],
      "timeout":{
        "backend": "10s",
        "cache": "8s"
      },
      "cache_name": "my_cache",
      "redis": {
        "found": true,
        "url": "127.0.0.1:6379"
      }
    },
    "http": {
      "servers": {
        "tls_terminator": {
          "listen": [
            ":443"
          ],
          "routes": [
            {
              "match": [
                {
                  "host": [
                    "localhost"
                  ]
                }
              ],
              "handle": [
                {
                  "handler": "cache"
                },
                {
                  "handler": "headers",
                  "response": {
                    "set": {
                      "Surrogate-Key": ["localhost-dev"]
                    }
                  }
                },
                {
                  "handler": "static_response",
                  "body": "Hello, world!",
                  "status_code": 200
                }
              ]
            }
          ],
          "logs": {}
        }
      }
    },
    "tls": {
      "automation": {
        "on_demand": {
          "ask": "http://localhost:3000/caddy"
        }
      },
      "cache": {
        "capacity": 100000
      }
    }
  },
  "admin": {
    "identity": {
      "issuers": [
        {
          "module": "acme",
          "email": "me@email.com"
        }
      ]
    }
  },
  "logging": {
    "logs": {
      "default": {
        "level": "DEBUG",
        "exclude": [
          "http.log.access"
        ],
        "writer": {
          "output": "file",
          "filename": "/var/log/caddy-dev/caddy.log",
          "roll": true,
          "roll_size_mb": 64,
          "roll_keep": 20
        },
        "encoder": {
          "format": "json",
          "time_format": "iso8601"
        }
      },
      "log0": {
        "level": "DEBUG",
        "writer": {
          "output": "file",
          "filename": "/var/log/caddy-dev/access.log",
          "roll": true,
          "roll_size_mb": 64,
          "roll_keep": 20
        },
        "encoder": {
          "format": "json",
          "time_format": "iso8601"
        },
        "include": [
          "http.log.access"
        ]
      }
    }
  },
  "storage": {
    "module": "file_system",
    "root": "~/caddy-dev/data"
  }
}

It’s quite a weird issue, I hope someone can help me :slight_smile: thanks guys!

cc @matt @darkweak just in case you know what this could be about.

Hey @Drago what is the version of Souin? How do you build it? What is the storage used (rueidis or redis)? Thanks for your answers.

2 Likes

Hey Sylvain, thanks for responding!

I use Docker + xcaddy without versions specified, so it pulls the latest version.

This is my Dockerfile:

FROM caddy:2.10.0-builder AS builder

RUN xcaddy build \
    --with github.com/ss098/certmagic-s3 \
    --with github.com/caddyserver/replace-response \
    --with github.com/caddy-dns/cloudflare \
    --with github.com/zhangjiayin/caddy-geoip2 \
    --with github.com/mholt/caddy-ratelimit \
    --with github.com/pberkel/caddy-storage-redis \
    --with github.com/caddyserver/cache-handler \
    --with github.com/darkweak/storages/go-redis/caddy

FROM caddy:2.10.0

# Install bash and curl
RUN apk update && apk add bash curl

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

# Copy the Caddy configuration file to the container
COPY config/caddy-dev.json /etc/caddy/caddy-dev.json

# Copy the start script and make it executable
COPY /bin/start.sh /usr/local/bin/start.sh
RUN chmod +x /usr/local/bin/start.sh

ENTRYPOINT ["/usr/local/bin/start.sh"]

My start.sh script basically only contains:

#!/bin/bash

# Start Caddy in the background
caddy run --config /etc/caddy/caddy.json &

# Wait for all background processes to finish
wait

I build this Docker image with:

docker build -t tls_reverse_proxy_dev -f Dockerfile .

Then I run it with:

docker run -d --name tls_reverse_proxy_dev \
  -p 80:80 -p 443:443 \
  -v ~/code/tls-reverse-proxy/tls_reverse_proxy_dev/data:/root/caddy-dev/data \
  -v ~/code/tls-reverse-proxy/tls_reverse_proxy_dev/log:/var/log/caddy-dev \
  -v ~/code/tls-reverse-proxy/tls_reverse_proxy_dev/config/caddy-dev.json:/etc/caddy/caddy-dev.json \
  tls_reverse_proxy_dev

@darkweak - I fixed this issue by building Caddy with

github.com/darkweak/souin/plugins/caddy instead of github.com/caddyserver/cache-handler. So basically just replace these two in my Dockerfile and it works.

I tested it multiple times now. Purging by surrogate key works perfectly with souin/plugins/caddy, but fails with caddy/cache-handler even though caddy/cache-handler returns the 204 code, tells you its purging the keys in logs, etc.

The github.com/caddyserver/cache-handler module/repo is usually a bit behind its counterpart in souin. I think Sylvain treats them something like edge and stable.

1 Like