Closed ytilotti closed 4 months ago
The problems appear when mercure-turbo-stream is set to true.
"@symfony/ux-turbo": {
"turbo-core": {
"enabled": true,
"fetch": "eager"
},
"mercure-turbo-stream": {
"enabled": true,
"fetch": "eager"
}
}
I don't know if it's the root cause, but .dev
is a real TLD with HSTS settings etc.
It is known to cause issues in dev.
Could you try with .test
or .localhost
, which are designed for dev use cases instead?
Thanks for your answer @dunglas but same issue with Caddyfile.test.
The calls:
Main page /chat:
First call mercure:
Second call mercure: We can see on the second call that the cookie is not sent.
If I call directly mercure, it's ok:
I will add some code to maybe have more informations.
Dockerfile:
mercure:
image: dunglas/mercure:v0.14.1
volumes:
- ./.docker/mercure/Caddyfile:/etc/caddy/Caddyfile.test:ro
- ./.docker/certs/:/etc/caddy/certs/:ro
environment:
MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_JWT_SECRET:-!ChangeThisSecretItNeedsToBeMuchLonger!}
MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_JWT_SECRET:-!ChangeThisSecretItNeedsToBeMuchLonger!}
command: /usr/bin/caddy run --config /etc/caddy/Caddyfile.test
ports:
- target: 3001
published: 3001
protocol: tcp
hostname: mercure.xxxxxxxxxxxxx.local
Caddyfile:
{
{$GLOBAL_OPTIONS}
}
{$SERVER_NAME:mercure.xxxxxxxxxxxxx.local:3001}
log {
format filter {
wrap console
fields {
uri query {
replace authorization REDACTED
}
}
}
}
tls /etc/caddy/certs/xxxxxxxxxxxxx.crt /etc/caddy/certs/xxxxxxxxxxxxx.key
{$EXTRA_DIRECTIVES}
route {
encode zstd gzip
mercure {
# Transport to use (default to Bolt)
transport_url {$MERCURE_TRANSPORT_URL:bolt://mercure.db}
# Publisher JWT key
publisher_jwt {$MERCURE_PUBLISHER_JWT_KEY} {$MERCURE_PUBLISHER_JWT_ALG}
# Subscriber JWT key
subscriber_jwt {$MERCURE_SUBSCRIBER_JWT_KEY} {$MERCURE_SUBSCRIBER_JWT_ALG}
# Extra directives
cors_origins https://extranet.xxxxxxxxxxxxx.local https://xxxxxxxxxxxxx.local
{$MERCURE_EXTRA_DIRECTIVES}
}
respond /healthz 200
respond "Not Found" 404
}
.env:
###> symfony/mercure-bundle ###
# See https://symfony.com/doc/current/mercure.html#configuration
# The URL of the Mercure hub, used by the app to publish updates (can be a local URL)
MERCURE_URL=https://mercure.xxxxxxxxxxxxx.local:3001/.well-known/mercure
# The public URL of the Mercure hub, used by the browser to connect
MERCURE_PUBLIC_URL=https://mercure.xxxxxxxxxxxxx.local:3001/.well-known/mercure
# The secret used to sign the JWTs
MERCURE_JWT_SECRET="!ChangeThisSecretItNeedsToBeMuchLonger!"
###< symfony/mercure-bundle ###
mercure.yaml:
mercure:
hubs:
default:
url: '%env(MERCURE_URL)%'
public_url: '%env(MERCURE_PUBLIC_URL)%'
jwt:
secret: '%env(MERCURE_JWT_SECRET)%'
publish: '*'
controllers.json:
{
"controllers": {
"@symfony/ux-turbo": {
"turbo-core": {
"enabled": true,
"fetch": "eager"
},
"mercure-turbo-stream": {
"enabled": true,
"fetch": "eager"
}
}
},
"entrypoints": []
}
ChatController.php:
<?php
namespace App\Controller\Admin;
use App\Controller\Controller as AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mercure\Authorization;
use Symfony\Component\Mercure\Discovery;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/pro_admin201')]
class ChatController extends AbstractController
{
#[Route('/chat', name: 'admin_chat')]
public function chat(
Request $request,
HubInterface $hub,
Discovery $discovery,
Authorization $authorization,
): Response
{
$form = $this->createFormBuilder()
->add('message', TextType::class, ['attr' => ['autocomplete' => 'off']])
->add('send', SubmitType::class)
->getForm();
$emptyForm = clone $form; // Used to display an empty form after a POST request
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
// 🔥 The magic happens here! 🔥
// The HTML update is pushed to the client using Mercure
$hub->publish(new Update(
'https://example.com/books/1',
$this->renderView('admin/chat/message.stream.html.twig', ['message' => $data['message']]),
));
// Force an empty form to be rendered below
// It will replace the content of the Turbo Frame after a post
$form = $emptyForm;
}
return $this->render('admin/chat/index.html.twig', [
'form' => $form,
]);
}
}
chat/index.html.twig:
{% extends "admin/layout.html.twig" %}
{% block title %}Messages {{ parent() }}{% endblock %}
{% block javascripts %}
<script>
const eventSource = new EventSource("{{ mercure('https://example.com/books/1', { subscribe: ['https://example.com/books/1'] })|escape('js') }}", {
withCredentials: true
});
</script>
{% endblock %}
{% block body %}
<h1>Chat</h1>
<div id="messages" {{ turbo_stream_listen('https://example.com/books/1') }}>
{#
The messages will be displayed here.
"turbo_stream_listen()" automatically registers a Stimulus controller that subscribes to the "chat" topic as managed by the transport.
All connected users will receive the new messages!
#}
</div>
<turbo-frame id="message_form">
{{ form(form) }}
{#
The form is displayed in a Turbo Frame, with this trick a new empty form is displayed after every post,
but the rest of the page will not change.
#}
</turbo-frame>
{% endblock %}
chat/message.stream.html.twig:
<turbo-stream action="append" target="messages">
<template>
<div>{{ message }}</div>
</template>
</turbo-stream>
The cookie is missing on the second call. This is because the default Turbo Stream controller doesn't support cookies: https://github.com/symfony/ux-turbo/blob/2.x/assets/src/turbo_stream_controller.ts#L43
Currently, you have to use a custom Turbo Stream controller that sets the withCredentials: true
attributes to EventSource
, but that would be nice to support this natively in UX Turbo.
so @dunglas, this code is useless?
{% block javascripts %}
<script>
const eventSource = new EventSource("{{ mercure('https://example.com/books/1', { subscribe: ['https://example.com/books/1'] })|escape('js') }}", {
withCredentials: true
});
</script>
{% endblock %}
@ytilotti yes.
The EventSource
instance is provided in the stimulus controller.
With a custom Turbo Stream controller it's working. It's really strange that this is not implemented by default because it's not in line with the documentation:
Keep in mind that you can use all features provided by Symfony Mercure, including private updates (to ensure that only authorized users will receive the updates) and async dispatching with Symfony Messenger.
It's wrong. There is a problem on the documention, no?
You can using a custom controller... But instead of fixing the docs we should fix the code.
I'll try to open a PR (but don't hesitate to do it if you prefer).
There are different ways to fix this:
turbo_stream_listen()
turbo_stream_listen()
turbo_stream_listen()
With the others functions on Mercure, the boolean seems to be the most logical. Link to https://github.com/symfony/ux/issues/291. If you give the way I can do the PR @dunglas. Thanks for your time anyway.
I would go with a new object argument passed as a parameter of turbo_stream_listen()
that will then be encoded as a JS object passed to EventSource
. This will also allow us to leverage the extra options added by polyfills (like custom headers).
Closing this issue in favor of https://github.com/symfony/ux/issues/291.
But how to integrate the mercure()
Twig function in the stimulus controller?
Hi all.
I have an issue 401 Unauthorized with subdomains on Symfony mercure. If I use theses domains, all is fine:
But with these following domains, the cookie is not set to the request headers for mercure:
More informations:
I miss something or a real issue? Seems to be fixed here #75