-
-
Notifications
You must be signed in to change notification settings - Fork 234
feat(api): enable HTTP cache with Souin via FrankenPHP/Caddy #619
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
aba62cc
feat(api): enable HTTP cache with Souin via FrankenPHP/Caddy
vincentchalamon 0934d42
fix(api): ignore Souin purge failures in CLI context and fix Dockerfi…
vincentchalamon 64d8c4a
fix(api): apply Rector suggestions and disable cache in E2E tests
vincentchalamon 368609a
refactor(api): address review comments on PR #619
vincentchalamon f6f9d5b
refactor(api): switch to xcaddy to build FrankenPHP with custom modules
vincentchalamon 336b91c
fix(api): install xcaddy with CGO_ENABLED=0
vincentchalamon 4fea88d
fix(api): use full path for xcaddy binary
vincentchalamon 8256f59
fix(api): move when@prod block to end of api_platform.yaml
vincentchalamon f9f4bc2
fix(api): fix br encoder
vincentchalamon 7b4db42
fix(api): use github.com/dunglas/caddy-cbrotli for brotli encoder
vincentchalamon File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\HttpCache; | ||
|
|
||
| use ApiPlatform\HttpCache\PurgerInterface; | ||
| use Psr\Log\LoggerInterface; | ||
|
|
||
| /** | ||
| * Wraps the Souin purger to prevent cache invalidation failures from crashing the application. | ||
| * | ||
| * When the Caddy admin API is unreachable (e.g. during CLI commands like doctrine:fixtures:load), | ||
| * exceptions are caught and logged as warnings instead of propagating. | ||
| */ | ||
| final readonly class FaultTolerantSouinPurger implements PurgerInterface | ||
| { | ||
| public function __construct( | ||
| private PurgerInterface $inner, | ||
| private LoggerInterface $logger, | ||
| ) { | ||
| } | ||
|
|
||
| public function purge(array $iris): void | ||
| { | ||
| try { | ||
| $this->inner->purge($iris); | ||
| } catch (\Throwable $throwable) { | ||
| $this->logger->warning('Failed to purge HTTP cache: {message}', [ | ||
| 'message' => $throwable->getMessage(), | ||
| 'exception' => $throwable, | ||
| ]); | ||
| } | ||
| } | ||
|
|
||
| public function getResponseHeaders(array $iris): array | ||
| { | ||
| return $this->inner->getResponseHeaders($iris); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # HTTP Cache with Souin | ||
|
|
||
| * Status: accepted | ||
| * Deciders: @vincentchalamon | ||
|
|
||
| ## Context and Problem Statement | ||
|
|
||
| The demo application exposes public HTTP cache headers (`Cache-Control: public`) on API responses, but no cache layer actually stores and serves cached responses. Every request hits the PHP backend, even for unchanged resources. The demo needs an HTTP cache that integrates natively with FrankenPHP (Caddy) and API Platform's automatic cache invalidation, while also serving as an example for distributed deployments. | ||
|
|
||
| ## Considered Options | ||
|
|
||
| **Varnish** is the traditional HTTP cache for API Platform projects. It is battle-tested and well-documented. However, it requires a separate service (reverse proxy) in front of the application, adding operational complexity. It does not integrate natively with Caddy/FrankenPHP — the two processes must be orchestrated separately, and Varnish uses its own configuration language (VCL). | ||
|
|
||
| **Souin** is an HTTP cache module written in Go that compiles directly into Caddy as a plugin. Since FrankenPHP is built on Caddy, Souin runs in the same process — no additional service, no network hop for cache lookups. It is RFC 7234 compliant, supports surrogate key invalidation (which API Platform uses natively via `SouinPurger`), and offers pluggable storage backends. The trade-off is that it requires a custom FrankenPHP build via `xcaddy`. | ||
|
|
||
| **CloudFlare / CDN** would offload caching entirely to an external provider. This works well in production but cannot be demonstrated locally, and ties the demo to a specific vendor. | ||
|
|
||
| ## Decision Outcome | ||
|
|
||
| We chose **Souin** because it integrates natively with the existing FrankenPHP/Caddy stack — no extra service to manage, no network hop, and built-in support in API Platform via `api_platform.http_cache.purger.souin`. This makes the demo a realistic showcase of HTTP caching with automatic invalidation. | ||
|
|
||
| For storage, we use a **two-tier architecture: Otter (L1) + Redis (L2)**. Otter is an in-memory cache local to each process, providing the fastest possible read path. Redis is a shared, persistent store that serves two purposes: distributing the cache across multiple pods in a Kubernetes deployment, and surviving process restarts so the cache is not lost on every deploy. This combination demonstrates a production-ready pattern for distributed projects, which is a key goal of the demo application. | ||
|
|
||
| The cache is compiled into all images (dev and prod) but **only activated in production** via environment variables. This keeps the developer experience unchanged while ensuring the production setup is always tested with the same binary. | ||
|
|
||
| ## Links | ||
|
|
||
| * [Souin — HTTP cache module for Caddy](https://github.com/darkweak/souin) | ||
| * [Souin Caddy plugin documentation](https://docs.souin.io/docs/middlewares/caddy/) | ||
| * [API Platform HTTP Cache documentation](https://api-platform.com/docs/core/performance/) | ||
| * [FrankenPHP custom builds](https://frankenphp.dev/docs/compile/) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| {{- if .Values.redis.enabled }} | ||
| apiVersion: apps/v1 | ||
| kind: Deployment | ||
| metadata: | ||
| name: {{ include "api-platform.fullname" . }}-redis | ||
| labels: | ||
| {{- include "api-platform.labels" . | nindent 4 }} | ||
| app.kubernetes.io/component: redis | ||
| spec: | ||
| replicas: 1 | ||
| selector: | ||
| matchLabels: | ||
| {{- include "api-platform.selectorLabels" . | nindent 6 }} | ||
| app.kubernetes.io/component: redis | ||
| template: | ||
| metadata: | ||
| labels: | ||
| {{- include "api-platform.selectorLabels" . | nindent 8 }} | ||
| app.kubernetes.io/component: redis | ||
| spec: | ||
| {{- with .Values.imagePullSecrets }} | ||
| imagePullSecrets: | ||
| {{- toYaml . | nindent 8 }} | ||
| {{- end }} | ||
| containers: | ||
| - name: redis | ||
| image: "{{ .Values.redis.image.repository }}:{{ .Values.redis.image.tag }}" | ||
| imagePullPolicy: {{ .Values.redis.image.pullPolicy }} | ||
| ports: | ||
| - name: redis | ||
| containerPort: 6379 | ||
| protocol: TCP | ||
| readinessProbe: | ||
| exec: | ||
| command: ["redis-cli", "ping"] | ||
| initialDelaySeconds: 5 | ||
| periodSeconds: 10 | ||
| livenessProbe: | ||
| exec: | ||
| command: ["redis-cli", "ping"] | ||
| initialDelaySeconds: 5 | ||
| periodSeconds: 10 | ||
| resources: | ||
| {{- toYaml .Values.redis.resources | nindent 12 }} | ||
| {{- end }} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| {{- if .Values.redis.enabled }} | ||
| apiVersion: v1 | ||
| kind: Service | ||
| metadata: | ||
| name: {{ include "api-platform.fullname" . }}-redis | ||
| labels: | ||
| {{- include "api-platform.labels" . | nindent 4 }} | ||
| app.kubernetes.io/component: redis | ||
| spec: | ||
| type: ClusterIP | ||
| ports: | ||
| - port: 6379 | ||
| targetPort: redis | ||
| protocol: TCP | ||
| name: redis | ||
| selector: | ||
| {{- include "api-platform.selectorLabels" . | nindent 4 }} | ||
| app.kubernetes.io/component: redis | ||
| {{- end }} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.