Skip to content

fix(api): vary Souin cache key on Accept to fix admin API doc discovery - #682

Merged
vincentchalamon merged 1 commit into
4.3from
fix/souin-cache-accept-key
Aug 7, 2026
Merged

fix(api): vary Souin cache key on Accept to fix admin API doc discovery#682
vincentchalamon merged 1 commit into
4.3from
fix/souin-cache-accept-key

Conversation

@vincentchalamon

@vincentchalamon vincentchalamon commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Problem

https://demo.api-platform.com/admin fails to load after signing in as admin, with the console error:

Uncaught Error: Cannot fetch API documentation. An empty response was received for the entrypoint URL. Have you verified that CORS is correctly configured in your API?

Root cause

The admin runs in Hydra mode and calls parseHydraDocumentation(window.origin), i.e. a GET / with Accept: application/ld+json.

/ is content-negotiated: browsers (Accept: text/html) get the PWA homepage, API clients (Accept: application/ld+json) get the Hydra entrypoint. The Souin HTTP cache added in #619 keys / as GET-<host>-/ without including Accept, so the first cached representation (the PWA HTML, whose Vary does not list Accept) is served for every request, including the admin's ld+json entrypoint fetch.

api-doc-parser's fetchJsonLd then sees content-type: text/html, returns no body, and parseHydraDocumentation throws "An empty response was received for the entrypoint URL". The CORS hint in the message is generic and misleading; CORS is not involved.

Observed on production (read-only):

GET / with Accept: application/ld+json Response
normal text/htmlcache-status: Souin; hit; key=GET-...-/ (no Accept in key)
with a cache-busting query (different key) application/ld+json — the real API entrypoint

Fix

Add key { headers Accept } to the Souin cache {} block so content-negotiated URLs (/, /books, ...) are cached per representation. headers is additive to the default key (method+host+path+query). Applied to both the Helm chart values and compose.prod.yaml for parity.

Only Accept is required. Responses already emit Vary: Accept (from content negotiation) plus Content-Type, Authorization, Origin (from api_platform.http_cache.vary) and Accept-Encoding (Caddy encode); Souin honors these per RFC 7234, so per-user/-origin variants stay isolated without putting Authorization in the key (which would fragment the shared cache per token). The bug was specifically the cross-backend collision at / (PWA HTML vs API JSON-LD), which only Accept in the key resolves.

Verification

helm template confirms the rendered caddy-global-options ConfigMap value now contains the key { headers Accept } block.

Tradeoff / follow-ups

  • Keying globally on Accept fragments cached entries by the raw Accept value (browsers vary it), lowering hit-rate on non-negotiated resources. Acceptable for the demo; a narrower cache_keys scope or a Vary-based approach (PWA emitting Vary: Accept) would avoid it but is broader.
  • The Redis L2 store runs with the default noeviction policy at a 128Mi cap; a wider keyspace mildly raises OOM risk. Consider --maxmemory + allkeys-lru as a separate hardening.

Note

Production is deployed via Flux from a separate GitOps repo. If that repo overrides php.caddyGlobalOptions instead of inheriting the chart default, the same key { headers Accept } one-liner must be added to its Souin block for the fix to reach production.

Refs #681

The admin loads in Hydra mode and calls parseHydraDocumentation(window.origin),
i.e. a GET / with `Accept: application/ld+json`. Souin caches / under the key
`GET-...-/` without including Accept, so the first cached representation (the PWA
HTML homepage) is served for every request, including the admin's ld+json
entrypoint fetch. api-doc-parser receives text/html, gets no JSON-LD body, and
throws "An empty response was received for the entrypoint URL", leaving the admin
stuck on "This page couldn't load".

Add `key { headers Accept }` to the Souin cache block so content-negotiated URLs
(/, /books, ...) are cached per representation. Applied to both the Helm chart
values and compose.prod.yaml for parity.

Refs #681
@vincentchalamon
vincentchalamon force-pushed the fix/souin-cache-accept-key branch from 14a2fdb to 951039b Compare August 7, 2026 07:30
@vincentchalamon
vincentchalamon merged commit 4572ce6 into 4.3 Aug 7, 2026
7 checks passed
@vincentchalamon
vincentchalamon deleted the fix/souin-cache-accept-key branch August 7, 2026 07:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant