Skip to content

Stale-While-Revalidate background revalidation fails with "context canceled" in Caddy plugin #699

@esyon

Description

@esyon

When using Souin as a Caddy plugin, the stale-while-revalidate (SWR) feature partially works: stale content is served correctly, but the background revalidation always fails with "context canceled". This happens because Caddy cancels the request context immediately after the main response is sent, before the background goroutine can complete the upstream request.

Environment

  • Souin version: Tested with PR feat(rfc): stale-while-revalidate #690 commits (5dfcbdde and 2e1f6692)
  • Caddy version: Latest via xcaddy
  • Go version: 1.23.5
  • Storage backend: Dragonfly (Redis-compatible)
  • Platform: Docker/Linux

Caddyfile Configuration

cache {
    ttl 20s
    stale 10s
    default_cache_control "public, max-age=20, stale-while-revalidate=10"
    timeout {
        backend 60s
        cache 60s
    }
    storers redis
    redis {
        configuration {
            Addrs dragonfly:6379
            DB 1
        }
    }
}

reverse_proxy backend:3000

Steps to Reproduce

  1. Make initial request → Cache stores response (TTL=20s, stale=10s)
  2. Wait 22 seconds (past TTL, within stale window)
  3. Make second request with Cache-Control: max-stale=300

Expected Behavior

  • Stale response returned immediately ✅
  • Background revalidation fetches fresh content from upstream
  • Cache updated with fresh content

Actual Behavior

  • Stale response returned immediately ✅
  • Background revalidation fails instantly with "context canceled" ❌
  • Cache NOT updated; next request triggers another cache miss

Debug Logs

Cache-Status: Souin; hit; ttl=-3; key=GET-/--de-default; detail=REDIS

DEBUG   http.handlers.cache     Found at least one valid response in the REDIS storage
DEBUG   http.handlers.cache     Revalidate the request with the upstream server
DEBUG   http.handlers.reverse_proxy     selected upstream       {"dial": "storefront-xxx-dev:3000", "total_upstreams": 1}
DEBUG   http.handlers.reverse_proxy     upstream roundtrip      {"upstream": "storefront-xxx-dev:3000", "duration": 0.000108304, "request": {"remote_ip": "172.20.0.5", "remote_port": "48446", "client_ip": "172.20.0.5", "proto": "HTTP/1.1", "method": "GET", "host": "shop.apps.xxx.io", "uri": "/", "headers": {"Cache-Control": ["max-stale=300"], "Date": ["Fri, 23 Jan 2026 13:26:06 UTC"], "X-Forwarded-Server": ["4aedd6306c31"], "X-Forwarded-Host": ["shop.apps.xxx.io"], "X-Forwarded-For": ["172.20.0.5"], "Via": ["1.1 Caddy"], "User-Agent": ["Mozilla/5.0"], "X-Forwarded-Port": ["80"], "X-Forwarded-Proto": ["http"], "X-Real-Ip": ["172.20.0.5"], "Accept-Encoding": ["gzip"], "Accept": ["*/*"]}}, "error": "context canceled"}

Note the duration: 4 microseconds — the request fails instantly, not from a timeout.

Root Cause Analysis

We traced this extensively:

  1. The SWR goroutine in middleware.go clones the request with context.Background() correctly
  2. However, Caddy's reverse_proxy handler internally captures the original request's context during handler chain setup
  3. When Souin sends the main response, Caddy cancels the original context
  4. The background revalidation call through next(bgWriter, bgReq) reaches reverse_proxy, which still uses the (now canceled) original context
  5. The upstream roundtrip fails immediately

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions