feat: upgrade to Mercure 1.0 alpha 3 - #2611
Conversation
Publish-side validation is part of the protocol now: report what Update.Validate() rejects (reserved topics and event types, forbidden event IDs, invalid UTF-8 data) as a ValueError carrying the reason, and the dispatch failures as a RuntimeException. The subscribe query parameter becomes "match", so hot reloading advertises its topic with it. The publisher_jwt and subscriber_jwt directives now require the compatibility mode, which drops the audience, expiration and issuer checks: bind the keys to a trusted issuer instead.
The publisher_jwt and subscriber_jwt directives only work in the compatibility mode of the hub, which FrankenPHP is built without. Octane writes every entry of the mercure array as a Caddyfile line, so the issuer block is passed as a multi-line value.
6cacc6a to
2aacd4c
Compare
alexandre-daubois
left a comment
There was a problem hiding this comment.
I know it's a draft so feel free close anything that was already planned to be fixed 馃檪
| $provider = new \Symfony\Component\Mercure\Jwt\FactoryTokenProvider( | ||
| $jwFactory, | ||
| [new \Symfony\Component\Mercure\Jwt\Grant([\Symfony\Component\Mercure\Jwt\Grant::ACTION_PUBLISH], ['*'])], | ||
| ['iss' => 'https://localhost', 'aud' => 'https://localhost/.well-known/mercure'], |
There was a problem hiding this comment.
This passes only iss and aud as additional claims, and LcobucciFactory in ProtocolVersion::V1 mode throws InvalidArgumentException from JwtClaims::buildAuthorizationDetails() when sub or client_id is missing, so $hub->publish() throws before sending anything. You should add sub and client_id to the claims array of the example.
| } | ||
|
|
||
| return nil, 1 | ||
| return nil, C.CString(err.Error()), C.FRANKENPHP_MERCURE_INVALID_UPDATE |
There was a problem hiding this comment.
This surfaces the raw GoPackedArray error, so mercure_publish(['foo', 1]) throws ValueError: cannot cast value of type int64 to type string, a Go type name with no Argument #1 ($topics) context. You could check the element types in C with zend_argument_type_error(1, ...) before crossing into Go, as the array|string check does.
|
|
||
| // The protocol requires access tokens to name their issuer, so the keys are | ||
| // bound to one trusted issuer instead of being set globally. | ||
| issuer := os.Getenv("MERCURE_TRUSTED_ISSUERS") |
There was a problem hiding this comment.
No file under docs/ mentions this new variable, its https://localhost default or the new hard requirement on MERCURE_SUBSCRIBER_JWT_KEY, and the switch to RFC 9068 tokens means every publisher token that worked with php-server --mercure before the upgrade now fails on an untrusted issuer, a missing exp or a missing at+jwt type. It may be worth a paragraph in docs/mercure.md listing the variables --mercure reads and the claims a token needs.
|
|
||
| // The protocol constrains topics, id, type and data: report the violations | ||
| // as argument errors instead of as a failed publication. | ||
| if err := u.Validate(); err != nil { |
There was a problem hiding this comment.
This runs Validate() a second time on every publish, because Hub.Publish() calls it first thing before dispatching, and one pass costs about 6 碌s and 185 allocations for two topics since addressesReservedNamespace() parses each topic as a WHATWG URL. You can drop this call and classify the Publish() error with errors.Is against the exported sentinels (ErrMissingTopic, ErrReservedTopic, ErrInvalidData, ...), which keeps the split in step with any rule the hub adds in a later alpha.
| case 2: | ||
| zend_throw_exception(spl_ce_RuntimeException, "Publish failed", 0); | ||
| case FRANKENPHP_MERCURE_INVALID_UPDATE: | ||
| zend_value_error("%s", result.r1); |
There was a problem hiding this comment.
This raises the hub message bare, "/.well-known/mercure/subscriptions": topic value resolves into the reserved namespace, while the $retry check yields the standard mercure_publish(): Argument #6 ($retry) must be ... shape, so the two ValueErrors of one function read differently. The hub exports one sentinel per rule, so you could map them to an argument number on the Go side (topics 1, data 2, id 4, type 5) and raise through zend_argument_value_error().
| assert.Contains(t, body, "update 1: ") | ||
| assert.Contains(t, body, "update 2: ") | ||
| // Updates rejected by the protocol are reported as argument errors. | ||
| assert.Contains(t, body, `error 1: "/.well-known/mercure/subscriptions"`) |
There was a problem hiding this comment.
These assertions cover the two ValueError paths only, and nothing in the suite hits FRANKENPHP_MERCURE_NO_HUB, FRANKENPHP_MERCURE_PUBLISH_FAILED, the nomercure build or the non-string array case at mercure.go:51, all of which this PR rewrote. A mercure_publish('foo') in a request without WithMercureHub and a mercure_publish([1]) in testdata/mercure-publish.php would cover two of them.
|
|
||
| // add the hot reload to the env variables | ||
| // "match" is the exact topic matcher of the Mercure protocol, the "topic" | ||
| // parameter it replaces is only honored in compatibility mode. |
There was a problem hiding this comment.
This overstates what compatibility mode does: without the deprecated_topic build tag, which FrankenPHP never sets, appendDeprecatedTopicMatchers() returns errTopicParamCompatNotSupported and the hub answers 400 to ?topic= in every mode. You could say the parameter needs a hub built with the deprecated_topic tag.
| ['iss' => 'https://localhost', 'aud' => 'https://localhost/.well-known/mercure'], | ||
| ); | ||
|
|
||
| $hub = new \Symfony\Component\Mercure\Hub('https://localhost/.well-known/mercure', $provider); |
There was a problem hiding this comment.
This constructs the Hub without protocolVersion: ProtocolVersion::V1, so it defaults to ProtocolVersion::Legacy and Authorization::setCookie() writes the 0.x mercureAuthorization cookie, which the 1.0 hub without deprecated_claim never reads. Publishing works either way, and the cookie-based subscriber authorization this section advertises is what breaks, so passing the version to the constructor keeps the example coherent.
| case 0: | ||
| switch (result.r2) { | ||
| case FRANKENPHP_MERCURE_OK: | ||
| if (result.r0 == NULL) { |
There was a problem hiding this comment.
Isn't it dead code with the bundled transports?
Upgrades
github.com/dunglas/mercureandgithub.com/dunglas/mercure/caddyfromv0.24.2tov1.0.0-alpha.3, and adapts the integration to the protocol changes of draft-dunglas-mercure-08.mercure_publish()The hub validates updates before dispatching them, so the function now distinguishes the caller's mistakes from a failed publication:
ValueErrorcarrying the reason for a topic addressing the reserved/.well-known/mercurenamespace or equal to*, an$idstarting with#or equal toearliest, a$typeequal tomercure, control characters, invalid UTF-8$data, or no topic at all;RuntimeExceptionwith the message of the hub when the dispatch fails.The negative values of
$retryare also rejected, as the protocol only allows digits in that field.An enum in
frankenphp.hreplaces the status codesgo_mercure_publish()returned as bare integers. On the way, an array of topics that cannot be converted no longer reportsNo Mercure hub configured.Hot reloading
The subscribe query parameter of the protocol is
match, so$_SERVER['FRANKENPHP_HOT_RELOAD']advertises/.well-known/mercure?match=<topic>. Thetopicparameter it replaces is only honored by a hub built with thedeprecated_topictag, which FrankenPHP doesn't set.Hub configuration
Setting
publisher_jwtorsubscriber_jwtwithoutprotocol_version_compatibilityis a configuration error now, because that mode also drops the requiredexp, the audience check, theat+jwtcheck and the issuer check. The keys are bound to a trusted issuer instead:php-server --mercureand the sampleCaddyfilefollow, and the access tokens using the 0.xmercureclaim are rejected: FrankenPHP is built without the compatibility tags of the hub.Docs
docs/mercure.mdand the Laravel Octane section ofdocs/laravel.mddescribe the 1.0 configuration. Octane writes each entry of itsmercurearray as aCaddyfileline, so theissuerblock is passed as a multi-line value (verified withcaddy adaptand a live hub). The Symfony example targets symfony/mercure 0.8, which shipsProtocolVersion::V1.The translations of
docs/mercure.mdanddocs/laravel.mdstill describe the 0.x configuration.