Skip to content

feat: support FrankenPHP 1.13 (Mercure 1.0) - #969

Open
ker0x wants to merge 5 commits into
dunglas:mainfrom
ker0x:feat/frankenphp-1.13
Open

ker0x wants to merge 5 commits into
dunglas:mainfrom
ker0x:feat/frankenphp-1.13

Conversation

@ker0x

@ker0x ker0x commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

FrankenPHP v1.13.0 embeds Mercure 1.0, which breaks the current configuration: the container fails to start.

Changes

  • Caddyfile: publisher_jwt / subscriber_jwt are now a configuration error outside of compatibility mode. The keys move into an issuer block, as in the upstream sample Caddyfile. The trusted issuer is configurable with the new MERCURE_TRUSTED_ISSUERS env var (defaults to https://localhost).
  • Caddyfile: the hub is named (name default). Mercure 1.0 allows only one unnamed hub per configuration, but the default SERVER_NAME (localhost, php:80) creates two servers, each with its own mercure handler. With the same name, both servers also share one transport, so updates published through http://php reach subscribers on https://localhost. default is the name the unnamed hub had internally, so existing data is kept.
  • Caddyfile: the audience is pinned with resource_identifier {$MERCURE_PUBLIC_URL}. In 1.0 mode the hub derives the expected aud from each request, so tokens issued for the public URL would be rejected when the app publishes through http://php.
  • compose.yaml: MERCURE_TRUSTED_ISSUERS (hub) and MERCURE_JWT_ISSUER (app, used by the [symfony/mercure-bundle] Use the Mercure 1.0 hub with bundle 0.5 and pin older recipes to Mercure 0.x symfony/recipes#1589 recipe) are both set from CADDY_MERCURE_JWT_ISSUER (defaults to https://localhost), so the token iss and the trusted issuer stay in sync.
  • compose.override.yaml: the demo directive was removed in Mercure 1.0 and made the dev container crash-loop. It is replaced with playground (UI now at /.well-known/mercure/debug/).
  • CI: the reachability check uses the 1.0 match subscribe parameter instead of topic.
  • Docs: document MERCURE_TRUSTED_ISSUERS and MERCURE_PUBLIC_URL.

No protocol_version_compatibility: apps must use MercureBundle ≥ 0.6, which defaults to protocol_version: 1.0, issues tokens with iss/aud, and uses the mercure_access_token cookie in debug mode (matching playground).

Requirements

Testing

Ran the new Caddyfile against FrankenPHP v1.13.0 / Caddy 2.11.7, in both dev (playground) and prod modes:

  • home page, subscribe with ?match= and ?topic=: 200
  • debug UI: 200 in dev, 404 in prod

Ran the CI steps against a copy of the branch, with the default SERVER_NAME (localhost, php:80):

  • the container is healthy and the CI Mercure check returns 200
  • an update published through http://php reaches a subscriber on https://localhost

Ran a Symfony app on a copy of the branch, with mercure-bundle 0.6.0 and the mercure.yaml from symfony/recipes#1589:

  • publishing through http://php succeeds (401 without resource_identifier)
  • a private subscription with the bundle's mercure_access_token cookie receives the update; an anonymous subscriber doesn't

The 1-php8.5 base image tag now resolves to 1.13; this config doesn't work on 1.12, so existing users need docker compose build --pull.

- Move the Mercure JWT keys into an `issuer` block (configurable with
  `MERCURE_TRUSTED_ISSUERS`), as `publisher_jwt`/`subscriber_jwt` are
  now rejected outside of compatibility mode
- Enable `protocol_version_compatibility 8` so tokens issued by
  MercureBundle's default "0.x" protocol version keep working
- Replace the removed `demo` directive with `playground` in dev
- Use the `match` subscribe parameter in CI
@ker0x
ker0x force-pushed the feat/frankenphp-1.13 branch from 2a10c93 to 9c9c8d6 Compare October 5, 2026 08:01
Mercure 1.0 allows only one unnamed hub per configuration, but the
default SERVER_NAME ("localhost, php:80") creates two servers, each
with its own mercure handler. Naming the hub also makes both servers
share the same transport, so updates published through http://php
reach subscribers on https://localhost.
@akerbel

akerbel commented Oct 5, 2026

Copy link
Copy Markdown

It doesn't work in Firefox. https://localhost shows SEC_ERROR_BAD_SIGNATURE
But works as usual in Edge (certificate sucks, but you can skip it)

@ker0x

ker0x commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

I will check that, mine is working fine on Firefox but i'm using a custom TLD rather than localhost

shamisen-cat added a commit to shamisen-cat/stockflow-symfony that referenced this pull request Oct 5, 2026
- dunglas/symfony-docker#969 を取り込み
- issuer / protocol_version_compatibility 導入と demo→playground、CI の match 対応

Co-authored-by: Cursor <cursoragent@cursor.com>
@ker0x

ker0x commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

@akerbel If you added the certificate authority to your local machine's certificate store as described in the docs/tls.md, that is likely the root cause of the error!

Firefox still trusts the root of a previous Caddy local CA. When the caddy_data volume is recreated, Caddy generates a new root with the same name, and Firefox rejects the new chain with SEC_ERROR_BAD_SIGNATURE. To fix it, remove the "Caddy Local Authority - 2026 ECC Root" entry in Firefox (Settings → Privacy & Security → View Certificates → Authorities), then trust the current root again or import it into Firefox directly.

@dunglas

dunglas commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Two notes from the MercureBundle side:

  • In dev, the hub runs in playground mode and reads the mercure_access_token cookie, while MercureBundle 0.5 with protocol_version: 1.0 sets __Secure-mercure_access_token, so cookie-based subscriptions to private updates won't be authorized. Default protocol_version to 1.0 symfony/mercure-bundle#130 defaults the cookie name to mercure_access_token when kernel.debug is on, and protocol_version to 1.0.
  • Removing protocol_version_compatibility 8 will also need a matching aud. The bundle defaults aud to the hub's public URL (MERCURE_PUBLIC_URL, https://localhost:443/.well-known/mercure by default), but the app publishes through http://php/.well-known/mercure, and outside compatibility mode the hub derives the expected audience from each request. Pinning the audience in the Caddyfile should align both sides (not tested):
resource_identifier {$MERCURE_PUBLIC_URL}

MercureBundle 0.6 defaults to protocol version 1.0 and issues tokens with
iss/aud claims. Pin the hub's resource identifier to MERCURE_PUBLIC_URL so
tokens stay valid when the app publishes through http://php, and share the
issuer between the app (MERCURE_JWT_ISSUER) and the hub
(MERCURE_TRUSTED_ISSUERS).
@ker0x

ker0x commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @dunglas!

Tested locally with FrankenPHP 1.13, mercure-bundle 0.6.0 and the mercure.yaml from symfony/recipes#1589:

  • publishing from the app through http://php/.well-known/mercure works (and gets a 401 without resource_identifier, so your suggestion is needed);
  • a private subscription with the bundle's mercure_access_token cookie receives the update, and an anonymous subscriber doesn't;
  • the CI reachability check (?match=test) still returns 200.

This depends on symfony/recipes#1589: bundle 0.6 won't start without the iss/sub/client_id claims.

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.

3 participants