Closed NK0D1NG closed 1 year ago
Hi @NK0D1NG, I am unable to replicate this issue.
Your example code seemed quite complicated so I just set up a minimal example to confirm that I can subscribe to private channels.
I spun up a Soketi server with Docker, per their docs:
docker run -p 6001:6001 -p 9601:9601 quay.io/soketi/soketi:1.4-16-debian
I set up a fresh Laravel install with Laravel Breeze's Vue stack to scaffold out auth views.
I installed the Pusher and Echo packages:
composer require pusher/pusher-php-server
npm install --save-dev laravel-echo pusher-js
I configured my .env
as follows:
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=app-id
PUSHER_APP_KEY=app-key
PUSHER_APP_SECRET=app-secret
PUSHER_HOST=127.0.0.1
PUSHER_PORT=6001
PUSHER_SCHEME=http
PUSHER_APP_CLUSTER=mt1
I uncommented the default Echo configuration in resources/js/bootstrap.js
:
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_PUSHER_APP_KEY,
wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});
I then logged into the Laravel app and observed Echo connecting to Soketi:
I then uncommented App\Providers\BroadcastServiceProvider::class
in config.app
and set up a channel in routes/channels.php
:
Broadcast::channel('foo-channel', fn () => true);
I then created an event with artisan make:event FooEvent
and updated it to implement ShouldBroadcast
. I updated the constructor to accept a $message
and updated the broadcastOn
method to use the foo
channel:
class FooEvent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public string $message)
{
//
}
public function broadcastOn()
{
return new PrivateChannel('foo-channel');
}
}
In Dashboard.vue
I subscribed to the event on the private channel:
Echo.private('foo-channel')
.listen('FooEvent', (e) => {
console.log(e);
});
And observed that Echo made the auth request:
I then used Laravel Tinker to dispatch the event:
And saw it come through in the console:
Are you able to confirm whether the issue exists for you with a minimal setup like this? If not, is there something specific I should do to replicate the issue?
Hey @jessarcher thanks for investigating into this. The only difference to my setup could be the custom auth guard I am using:
from config/auth.php:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'vcs' => [
'driver' => 'session',
'provider' => 'clients',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
'clients' => [
'driver' => 'eloquent',
'model' => App\Models\Client::class,
],
],
from routes/channels.php:
Broadcast::channel('room.{ruuid}', function ($client, $ruuid) {
Log::info('Broadcasting.Channel: ' . $ruuid);
return $client->room_uuid == $ruuid;
}, ['guards' => ['vcs']]);
My app/Providers/BroadcastServiceProvider.php:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Broadcast::routes(['middleware' => 'web', 'guards' => 'vcs']);
require base_path('routes/channels.php');
}
}
This is also the reason why my Echo setup seems complicated. I am also using Laravel Sanctum with an external Vue SPA which authenticates using Laravel's default session driver as you can see. Everything is set up like described in the official docs and not the first time I am doing it like this. Because Laravel Sanctum requires the X-XSRF-TOKEN and the Session Cookie to be set and Laravel Echo does not send token/session cookie automatically I added it by myself using the custom authorizer. As well as the application/json header. The Vue SPA is a different repository/project. Authenticating using the custom guard works just fine (for normal API routes and for the channels as well as you can see in the images in my original post). Because I have no monorepo I wasn't able to setup a minimal example for this whole setup yet.
So maybe Echo has a problem with the custom guard?
Can you confirm a subscription message inside the websocket connection? What do your request/responses in the websocket connection look like? Is there a subscription/subscription succeeded message? Can you confirm it works in your minimal example using a custom guard and Laravel Sanctum? I am using just another Eloquent model like:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Spatie\Permission\Traits\HasRoles;
use App\Traits\Uuids;
class Client extends Authenticatable
{
use HasFactory, HasApiTokens, HasRoles, Uuids;
protected $guard_name = "vcs";
public function room()
{
return $this->belongsTo(Room::class, 'room_uuid', 'uuid');
}
}
I also use Redis as my queue driver.
I can also confirm that the event is correctly fired (the Laravel Event Listener gets called and I can see the event payload in the Soketi Logs) - so the only thing missing is the subscription call for the private channel from Laravel Echo..
By the way: I am not using axios - I am using the fetch API (The frontend is developed with NuxtJS which uses ohmyfetch as a fetch API dependency).
@driesvints @jessarcher I provided two demo repositories (links in the original post) which also demonstrate the issue. They should work out of the box after installing the dependencies. Please follow the README of the Laravel Backend Repo to do the setup and create a first user with a custom guard. Even with this simplified setup Laravel Echo does not subscribe to the private channel.
For the frontend just use the newly created user (model is called 'client') to login. Then click on the Join Room Chat button which calls the listen
-method - but the callback never gets executed and there is no subscription message in the websocket connection.
I was finally able to reproduce the issue and solve it. It is important to call the callback on the custom authorizer:
function setupEcho() {
const token = useCookie('XSRF-TOKEN').value
const config = useRuntimeConfig()
channelName.value = 'room.' + roomsStore.currentRoom.roomUuid
console.log('Channel Name: ' + channelName)
if (!echo.value) {
echo.value = new Echo({
authorizer: (channel, options) => {
return {
authorize: (socketId, callback) => {
const request = $fetch(config.baseURL + '/broadcasting/auth', {
async onRequest({ request, options }) {
options.method = 'post'
options.body = {
socket_id: socketId,
channel_name: channel.name
}
options.headers = {'X-XSRF-TOKEN': token,
'Accept': 'application/json',
},
options.credentials = 'include'
},
})
request.then((response) => {
console.log(response)
callback(null, response)
})
}
};
},
broadcaster: 'pusher',
key: 'app-key',
cluster: 'mt1',
wsHost: 'localhost',
wsPort: 6001,
forceTLS: false,
disableStats: true,
encrypted: true,
auth: {
headers: {'X-XSRF-TOKEN': token,
'Accept': 'application/json',
},
},
enabledTransports: ['ws', 'wss'],
});
}
}
This part is the important one:
request.then((response) => {
console.log(response)
callback(null, response)
})
I am not sure what callback is used internally by Laravel Echo but it is obligatory for the subscription of private/presence channels to work. All examples in the docs use axios so I had to change some code to work with the fetch API (which is used by the ohmyfetch library). After calling the callback (with a null value as first parameter which seems a bit strange but is also mentioned in the Laravel Docs) the subscription works and I get the event data:
This issue was not easy to debug, because the same setup works perfectly fine with public channels (including the subscription!) and there are absolutely no errors in the console when trying the same with private channels.
The issue can be closed.
Laravel 11: For those using a simple Sanctum/Echo/Reverb setup, you can try the following:
// config/cors.php
'paths' => ['api/*', 'sanctum/csrf-cookie',
'/broadcasting/auth' // add me
],
// routes/channels.php
use Illuminate\Support\Facades\Broadcast;
Broadcast::routes(['middleware' => ['api', 'auth:sanctum']]);
// main.js (of your SPA)
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
import axios from 'axios';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
authorizer: (channel, options) => ({
authorize: (socketId, callback) => {
axios.post(`${import.meta.env.VITE_API_URL}/broadcasting/auth`, {
socket_id: socketId,
channel_name: channel.name,
}, {
withCredentials: true, // this is
withXSRFToken: true, // important part
})
.then(response => {
callback(null, response.data);
})
.catch(error => {
callback(error);
});
},
}),
});
The default csrf configuration for Echo does not work out of the box with sanctum, as it tries to find the csrf token on the window object or in the DOM. We want to configure axios to be able to grab it from the cookie like it does on every other API request.
Since the /broadcasting/auth
route is now automatically published, the Broadcast::routes()
helper was removed from the docs. Curiously, the custom authorizer docs were removed as well. See change here.
This SO post helped me get here.
Description:
Laravel Echo is not subscribing to a private channel. After instantiating and authenticating Laravel Echo the EventListener ('listen'-Method) never gets called. In the websocket connection is no subscription message and Soketi does not receive any subscription message. Using a public channel works as expected.
Steps To Reproduce:
Frontend Code for 3.:
Frontend Code for 4.:
Examples for 5.: browser requests:
POST-data sent within the request to broadcasting/auth:
The response from the auth-Request:
This is the request/response-payload of the first (and only) websocket connection Laravel Echo creates:
The actual callback function inside the
listen
-handler never gets called from Laravel Echo. There is just nothing in the browser console. It seems like Laravel Echo does not even send a subscribe message when using private channels.If using the same event with a public channel Laravel Echo actually subscribes and I get the event data:
I just made a public 'test' channel on the backend and changed the event to broadcast on that channel. This is how I changed the Frontend Code:
I am not sure if there is a bug in Laravel Echo or maybe it is related to Soketi? Nevertheless I had the same problems using the laravel-websocket package, too so I think this bug is Laravel Echo related.
Repository to demonstrate the issue:
The frontend built with NuxtJS:
https://github.com/NK0D1NG/laravel-broadcasting-fe
The backend build with Laravel:
https://github.com/NK0D1NG/laravel-broadcasting-be
Related topics
https://laracasts.com/discuss/channels/laravel/laravel-echo-not-listening-to-events-using-it-with-laravel-homestead-could-it-be-a-compatibility-issue-with-vue-3
I also posted a lot of information about the problem in this post on Laracast (including more Laravel-specific backend code): https://laracasts.com/discuss/channels/laravel/event-listener-never-gets-called-even-if-the-event-gets-fired?page=1&replyId=849115