Trying to get some user context for private cache

1. The problem I’m having:

I’m trying to send a preflight request to obtain a User-Context-Hash header, which will then allow me to cache private data.

The cache always misses. I haven’t explored all the configuration options yet, but without the user context, the cache was working. I’m not sure if the cache can work within an intercept directive.

Could someone please provide me with some guidance to succeed, or confirm if it’s not possible, if that’s the case?

2. Error messages and/or full log output:

2025-01-15T06:24:02.751635477Z 2025/01/15 06:24:02.751	DEBUG	http.stdlib	http: TLS handshake error from 172.19.0.1:59044: remote error: tls: unknown certificate
2025-01-15T06:24:02.752428600Z 2025/01/15 06:24:02.752	DEBUG	events	event	{"name": "tls_get_certificate", "id": "fca022d7-f940-4ab5-813e-1f6d0c39e042", "origin": "tls", "data": {"client_hello":{"CipherSuites":[31354,4865,4866,4867,49195,49199,49196,49200,52393,52392,49171,49172,156,157,47,53],"ServerName":"localhost","SupportedCurves":[10794,4588,29,23,24],"SupportedPoints":"AA==","SignatureSchemes":[1027,2052,1025,1283,2053,1281,2054,1537],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[6682,772,771],"RemoteAddr":{"IP":"172.19.0.1","Port":59052,"Zone":""},"LocalAddr":{"IP":"172.19.0.9","Port":443,"Zone":""}}}}
2025-01-15T06:24:02.752448952Z 2025/01/15 06:24:02.752	DEBUG	tls.handshake	choosing certificate	{"identifier": "localhost", "num_choices": 1}
2025-01-15T06:24:02.752451449Z 2025/01/15 06:24:02.752	DEBUG	tls.handshake	custom certificate selection results	{"identifier": "localhost", "subjects": ["localhost"], "managed": false, "issuer_key": "", "hash": "eaef97f8d3bb92ba2b1671f45688a7271bfa97856eef560762546bf10372bd52"}
2025-01-15T06:24:02.752453916Z 2025/01/15 06:24:02.752	DEBUG	tls.handshake	matched certificate in cache	{"remote_ip": "172.19.0.1", "remote_port": "59052", "subjects": ["localhost"], "managed": false, "expiration": "2026/01/14 12:54:15.000", "hash": "eaef97f8d3bb92ba2b1671f45688a7271bfa97856eef560762546bf10372bd52"}
2025-01-15T06:24:02.754437656Z 2025/01/15 06:24:02.754	DEBUG	http.handlers.rewrite	rewrote request	{"request": {"remote_ip": "172.19.0.1", "remote_port": "59052", "client_ip": "172.19.0.1", "proto": "HTTP/2.0", "method": "GET", "host": "localhost", "uri": "index.php?page=1&itemsPerPage=2", "headers": {"Referer": ["https://localhost/api/docs"], "Priority": ["u=1, i"], "Cache-Control": ["no-cache"], "Accept": ["application/ld+json"], "Sec-Ch-Ua": ["\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\""], "Sec-Fetch-Site": ["same-origin"], "Sec-Fetch-Dest": ["empty"], "Sec-Ch-Ua-Platform": ["\"Linux\""], "Accept-Encoding": ["gzip, deflate, br, zstd"], "Accept-Language": ["fr,en;q=0.9"], "Pragma": ["no-cache"], "Authorization": ["REDACTED"], "User-Agent": ["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"], "Sec-Fetch-Mode": ["cors"], "Dnt": ["1"], "Sec-Ch-Ua-Mobile": ["?0"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "localhost"}}, "method": "GET", "uri": "index.php?page=1&itemsPerPage=2"}
2025-01-15T06:24:02.858786281Z 2025/01/15 06:24:02.858	ERROR	the current responseWriter is not a flusher	{"error": "feature not supported"}
2025-01-15T06:24:02.892363889Z 2025/01/15 06:24:02.892	DEBUG	http.handlers.intercept	handling response	{"handler": 0}
2025-01-15T06:24:02.892422451Z 2025/01/15 06:24:02.892	DEBUG	http.handlers.cache	Incomming request &{Method:GET URL:index.php?page=1&itemsPerPage=2 Proto:HTTP/2.0 ProtoMajor:2 ProtoMinor:0 Header:map[Accept:[application/ld+json] Accept-Encoding:[gzip, deflate, br, zstd] Accept-Language:[fr,en;q=0.9] Authorization:[user-pharma-admin] Cache-Control:[no-cache] Dnt:[1] Pragma:[no-cache] Priority:[u=1, i] Referer:[https://localhost/api/docs] Sec-Ch-Ua:["Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"] Sec-Ch-Ua-Mobile:[?0] Sec-Ch-Ua-Platform:["Linux"] Sec-Fetch-Dest:[empty] Sec-Fetch-Mode:[cors] Sec-Fetch-Site:[same-origin] User-Agent:[Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36]] Body:0xc000ea2a68 GetBody:<nil> ContentLength:0 TransferEncoding:[] Close:false Host:localhost Form:map[] PostForm:map[] MultipartForm:<nil> Trailer:map[] RemoteAddr:172.19.0.1:59052 RequestURI:index.php?page=1&itemsPerPage=2 TLS:0xc000e400c0 Cancel:<nil> Response:<nil> Pattern: ctx:0xc0020cf440 pat:<nil> matches:[] otherValues:map[]}
2025-01-15T06:24:02.892540556Z 2025/01/15 06:24:02.892	DEBUG	http.handlers.cache	Request cache-control &{MaxAge:-1 MaxStale:-1 MaxStaleSet:false MinFresh:-1 NoCache:true NoStore:false NoTransform:false OnlyIfCached:false StaleIfError:0 Extensions:[]}
2025-01-15T06:24:02.892549997Z 2025/01/15 06:24:02.892	DEBUG	http.handlers.cache	Request the upstream server
2025-01-15T06:24:03.106640298Z 2025/01/15 06:24:03.106	ERROR	the current responseWriter is not a flusher	{"error": "feature not supported"}
2025-01-15T06:24:03.133720302Z 2025/01/15 06:24:03.133	DEBUG	http.handlers.cache	Response cache-control &{MustRevalidate:false NoCache:map[] NoCachePresent:true NoStore:false NoTransform:false Public:true Private:map[] PrivatePresent:true ProxyRevalidate:false MaxAge:2147483647 SMaxAge:-1 Immutable:false StaleIfError:-1 StaleWhileRevalidate:-1 Extensions:[]}
2025-01-15T06:24:03.133733226Z 2025/01/15 06:24:03.133	DEBUG	http.handlers.cache	Store the response for GET-https-localhost-index.php?page=1&itemsPerPage=2{-VARY-}Origin:fixed;Accept:application/ld+json;User-Context-Hash:TODO with duration 596523h14m5.866413431s
2025-01-15T06:24:03.138696790Z 2025/01/15 06:24:03.138	DEBUG	http.handlers.cache	Store the new mapping for the key GET-https-localhost-index.php?page=1&itemsPerPage=2{-VARY-}Origin:fixed;Accept:application/ld+json;User-Context-Hash:TODO in Default
2025-01-15T06:24:03.138719937Z 2025/01/15 06:24:03.138	DEBUG	http.handlers.cache	Stored the key GET-https-localhost-index.php?page=1&itemsPerPage=2{-VARY-}Origin:fixed;Accept:application/ld+json;User-Context-Hash:TODO in the DEFAULT provider
2025-01-15T06:24:03.139476748Z 2025/01/15 06:24:03.139	DEBUG	http.handlers.cache	Store the tag /api/.well-known/genid/50407300e13f9a2f0dd7
2025-01-15T06:24:03.139501366Z 2025/01/15 06:24:03.139	DEBUG	http.handlers.cache	Store the tag /api/.well-known/genid/5926db2e8fa6f9ca0519
2025-01-15T06:24:03.139774749Z 2025/01/15 06:24:03.139	INFO	http.log.access.log0	handled request	{"request": {"remote_ip": "172.19.0.1", "remote_port": "59052", "client_ip": "172.19.0.1", "proto": "HTTP/2.0", "method": "GET", "host": "localhost", "uri": "/api/trades?itemsPerPage=2&page=1", "headers": {"Cache-Control": ["no-cache"], "Sec-Ch-Ua": ["\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\""], "Sec-Fetch-Site": ["same-origin"], "Sec-Ch-Ua-Platform": ["\"Linux\""], "Accept-Language": ["fr,en;q=0.9"], "Dnt": ["1"], "Referer": ["https://localhost/api/docs"], "Accept-Encoding": ["gzip, deflate, br, zstd"], "Pragma": ["no-cache"], "Accept": ["application/ld+json"], "Priority": ["u=1, i"], "Authorization": ["REDACTED"], "Sec-Fetch-Mode": ["cors"], "Sec-Fetch-Dest": ["empty"], "Sec-Ch-Ua-Mobile": ["?0"], "User-Agent": ["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "localhost"}}, "bytes_read": 0, "user_id": "", "duration": 0.385333389, "size": 1753, "status": 200, "resp_headers": {"Content-Encoding": ["zstd"], "Alt-Svc": ["h3=\":443\"; ma=2592000"], "Date": ["Wed, 15 Jan 2025 06:24:02 GMT", "Wed, 15 Jan 2025 06:24:03 GMT"], "X-Content-Type-Options": ["nosniff"], "X-Frame-Options": ["deny"], "Etag": ["\"faa2335d00513cb854bc8c5b6d04e1d2-zstd\""], "Server": ["Caddy"], "Content-Type": ["application/json", "application/ld+json; charset=utf-8"], "X-Debug-Token": ["7f36a7", "b11e8b"], "X-Debug-Token-Link": ["https://localhost/_profiler/7f36a7", "https://localhost/_profiler/b11e8b"], "X-Robots-Tag": ["noindex", "noindex"], "Link": ["<https://localhost/api/docs.jsonld>; rel=\"http://www.w3.org/ns/hydra/core#apiDocumentation\""], "Surrogate-Key": ["/api/.well-known/genid/50407300e13f9a2f0dd7, /api/.well-known/genid/5926db2e8fa6f9ca0519, /api/trades"], "Permissions-Policy": ["browsing-topics=()"], "Vary": ["Origin", "Accept", "User-Context-Hash", "Accept-Encoding"], "Cache-Status": ["Souin; fwd=uri-miss; stored; key=GET-https-localhost-index.php?page=1&itemsPerPage=2"]}}

3. Caddy version:

4. How I installed and ran Caddy:

a. System environment:

From docker image dunglas/frankenphp:1.3-php8.4-alpine.

d. My complete Caddy config:

{
	{$CADDY_GLOBAL_OPTIONS}

    debug

	frankenphp {
		{$FRANKENPHP_CONFIG}
	}

    cache {
        log_level debug
        mode bypass
        redis {
            configuration {
                url redis:6379
            }
        }
    }
}

{$CADDY_EXTRA_CONFIG}

{$SERVER_NAME:localhost} {
    tls /etc/caddy/server.crt /etc/caddy/server.key {
        key_type rsa2048
    }

	log {
		{$CADDY_SERVER_LOG_OPTIONS}
		# Redact the authorization query parameter that can be set by Mercure
		format filter {
			request>uri query {
				replace authorization REDACTED
			}
		}
		level DEBUG
	}

	root /app/public
	encode zstd br gzip

	{$CADDY_SERVER_EXTRA_DIRECTIVES}

	# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
	header ?Permissions-Policy "browsing-topics=()"

    intercept {
        @readyToCache {
            status 200
            header User-Context-Hash *
        }

        handle_response @readyToCache {
            request_header Origin fixed
            request_header User-Context-Hash {resp.header.User-Context-Hash}
            header -User-Context-Hash
            header -Cache-Control

            cache

            rewrite @phpRoute index.php
            php @frontController
        }

        handle_response {
        }
    }

    @phpRoute {
        not path /.well-known/mercure*
        not file {path}
    }
    rewrite @phpRoute index.php

    @file {
        file {path}
    }
    file_server @file

    @frontController path index.php
    php @frontController
}

5. Links to relevant resources:

directive intercept

I was able to get a hit with moving cache and second request in a handle directive. I will continue testing to verify that the cache only applies in the desired cases.

    intercept {
        @readyToCache {
            status 200
            header User-Context-Hash *
        }

        handle_response @readyToCache {
            request_header Origin fixed
            request_header User-Context-Hash {resp.header.User-Context-Hash}
            # Show for debug header -User-Context-Hash
            header -Cache-Control

            handle {
                cache

                rewrite @phpRoute index.php
                php @frontController
            }
        }
    }

2025/01/15 09:14:54.471	ERROR	the current responseWriter is not a flusher	{"error": "feature not supported"}
2025-01-15T09:14:54.494627015Z [2025-01-15T10:14:54.494419+01:00] doctrine.INFO: Disconnecting [] []
2025-01-15T09:14:54.494961401Z 2025/01/15 09:14:54.494	DEBUG	http.handlers.intercept	handling response	{"handler": 0}
2025-01-15T09:14:54.495028453Z 2025/01/15 09:14:54.495	DEBUG	http.handlers.cache	Incomming request &{Method:GET URL:index.php?page=1&itemsPerPage=10 Proto:HTTP/2.0 ProtoMajor:2 ProtoMinor:0 Header:map[Accept:[application/ld+json] Accept-Encoding:[gzip, deflate, br, zstd] Accept-Language:[fr,en;q=0.9] Authorization:[user-pharma-admin] Cache-Control:[no-cache] Dnt:[1] Origin:[fixed] Pragma:[no-cache] Priority:[u=1, i] Referer:[https://localhost/api/docs] Sec-Ch-Ua:["Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"] Sec-Ch-Ua-Mobile:[?0] Sec-Ch-Ua-Platform:["Linux"] Sec-Fetch-Dest:[empty] Sec-Fetch-Mode:[cors] Sec-Fetch-Site:[same-origin] User-Agent:[Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36] User-Context-Hash:[user-pharma-admin]] Body:0xc001076138 GetBody:<nil> ContentLength:0 TransferEncoding:[] Close:false Host:localhost Form:map[] PostForm:map[] MultipartForm:<nil> Trailer:map[] RemoteAddr:172.19.0.1:50030 RequestURI:index.php?page=1&itemsPerPage=10 TLS:0xc00159a0c0 Cancel:<nil> Response:<nil> Pattern: ctx:0xc00083ad80 pat:<nil> matches:[] otherValues:map[]}
2025-01-15T09:14:54.495253725Z 2025/01/15 09:14:54.495	DEBUG	http.handlers.cache	Request cache-control &{MaxAge:-1 MaxStale:-1 MaxStaleSet:false MinFresh:-1 NoCache:true NoStore:false NoTransform:false OnlyIfCached:false StaleIfError:0 Extensions:[]}
2025-01-15T09:14:54.496361046Z 2025/01/15 09:14:54.496	DEBUG	http.handlers.cache	The stored key GET-https-localhost-index.php?page=1&itemsPerPage=10{-VARY-}Origin:fixed;Accept:application/ld+json;User-Context-Hash:user-pharma-admin matched the current iteration key ETag &{Matched:true IfNoneMatchPresent:false IfMatchPresent:false IfModifiedSincePresent:false IfUnmodifiedSincePresent:false IfUnmotModifiedSincePresent:false NeedRevalidation:true NotModified:false IfModifiedSince:0001-01-01 00:00:00 +0000 UTC IfUnmodifiedSince:0001-01-01 00:00:00 +0000 UTC IfNoneMatch:[] IfMatch:[] RequestETags:[] ResponseETag:"6d6d6c90699b5b8fc76ac67f2a6c50ad"}
2025-01-15T09:14:54.496374629Z 2025/01/15 09:14:54.496	DEBUG	http.handlers.cache	Found at least one valid response in the DEFAULT storage