Search code examples
phplaraveldockerlaravel-reverb

Laravel Reverb not Broadcasting REST API with Docker


My Laravel 11 application is not broadcasting anything using Laravel Reverb. I am using a custom Docker configuration to spin up my enviornment and Laravel acts as a REST api interacting with a React stand-alone front-end.

My reverb.php file in my config/ directory is untouched as I just updated the .env.

BROADCAST_CONNECTION=reverb
    
REVERB_APP_ID=12345
REVERB_APP_KEY=MYVERYSECRETKEY
REVERB_APP_SECRET=SUPERSECRET
REVERB_HOST="localhost"
REVERB_PORT=9090
REVERB_SCHEME=http

The broadcasting.php file has this set as the driver as from what I gather Laravel Reverb runs it's own config reverb.php.

'default' => env('BROADCAST_DRIVER', 'null'),

When running php artisan queue:listen within my Docker container I can see the Events firing and everything running as it should...

When I run php artisan channel:list I see channel_for_everyone

When running within my Docker container php artisan reverb:start --debug I can see some ping logs.

Connection Established ............ 310096377.635725104  
Message Handled ................... 475763427.215883647  
Message Received .................. 726544741.227378338  
      
{ 
 "event": "pusher:ping", 
 "data": [] 
}

With the front-end in the Network tab again everything looks ok, it pings the /broadcasting/auth and my setup ws://localhost:9090/app/

Request URL: http://localhost:8000/broadcasting/auth
Request Method: POST
Status Code: 200 OK
Remote Address: [::1]:8000
Referrer Policy: strict-origin-when-cross-origin

Request URL: ws://localhost:9090/app/MYVERYSECRETKEY?protocol=7&client=js&version=8.4.0-rc2&flash=false
Request Method: GET OK
Status Code: 101 Switching Protocols

The connection itself seems ok? One thing to note is if I hit the endpoint to fire the event, I see no logs for the broacast php artisan reverb:start --debug

My Event, which is really basic for now and gets caught and logs when runnig queue:listen but never broadcasts, although the the broadcastOn() method gets hit and I can catch with a dd()

class CommentCreatedEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
    ) {
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        //dd('inside broadcaseOn() in Event'); <- Gets here
        return [
            new PrivateChannel('channel_for_everyone'), // can see value of channel
        ];
    }
}

Even if I make a channel not private I still get nothing!

public function broadcastOn()
{
    return [
        new Channel('channel_for_everyone'),
    ];
}

in my channels.php the dd() inside the channel_for_everyone never hits, but the file is 100% getting loaded as when I dd() above it will dd('it will hit here!);

dd('it will hit here!);
    
Broadcast::channel('channel_for_everyone', function ($user) {
  dd('callback hit for channel_for_everyone', $user); // Will not hit
  return true;
});

My React code is super basic, I made a hook to execute and it logs a successful connection, just no events are every logged.

const useLaravelEcho = () => {
    useEffect(() => {
        console.log('Initialising Echo...');

        const pusher = Pusher; // we need to make an instance

        const echoConfig = 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,
            forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
            enabledTransports: ['ws', 'wss'],
            authEndpoint: import.meta.env.VITE_AUTH_ENDPOINT,
        });

        // Listen for successful connection
        echoConfig.connector.pusher.connection.bind('connected', () => {
            console.log('Successfully connected to Echo server.');
        });

        // Listen for connection errors
        echoConfig.connector.pusher.connection.bind('error', (error: any) => {
            console.error('Error connecting to Echo server:', error);
        });

        console.log('Subscribing to channel...');
        echoConfig.private('channel_for_everyone')
            .listen('CommentCreatedEvent', (event: any) => {
                console.log('Event received:', event);
            });
    }, []);
};

export default useLaravelEcho;

When I check the console in browser:

Initialising Echo...
useLaravelEcho.ts:32 Subscribing to channel...
useLaravelEcho.ts:24 Successfully connected to Echo server.

I know this must be some kind of configuration error but I just cannot seem to find the issue as I have no errors or logs to go off!

Has anyone got any idea?


Solution

  • Fixed! There were a few issues in the end.

    Backend REST api:

    1. broadcasting.php ensuring that the BROADCAST_DRIVER is set to reverb as well as the BROADCAST_CONNECTION is set to reverb:

    This wasn't clear and the default options in the comments Supported: "pusher", "ably", "redis", "log", "null" do not state reverb as an option. I thought setting null would ignore this and my reverb config file would control this.

    1. I am dealing with JWT tokens to authenticate my users within the app and I needed to make a custom endpoint to handle this for me due to the nature of my setup.

    Code:

    public function reverb(ReverbRequest $request): Response
    {
        $socketId = $request->input('socket_id');
        $channelName = $request->input('channel_name');
    
        // this generates the required format for the response
        $stringToAuth = $socketId . ':' . $channelName;
        $hashed = hash_hmac('sha256', $stringToAuth, env('REVERB_APP_SECRET'));
        
        try {
            // Generate the required format for the response
            $stringToAuth = $socketId . ':' . $channelName;
            $hashed = hash_hmac('sha256', $stringToAuth, env('REVERB_APP_SECRET'));
    
            return response(['auth' => env('REVERB_APP_KEY') . ':' . $hashed]);
        } catch (Exception $e) {
            return response(['code' => 403, 'message' => 'Cannot authenticate reverb'], 403);
        }
    }
    

    Front end

    1. Pass the bearer token into the call to authenticate the user. I have also added a few improvements to the code here to.

    Code:

    const useLaravelEcho = () => {
        const [{ user }] = useUserContext();
        const { showNotificationToast } = useShowToast();
        const echoInstance = useRef<Echo | null>(null);
    
        useEffect(() => {
            if (!user) return;
    
            // Ensure only one Echo instance is created
            if (!echoInstance.current) {
                // eslint-disable-next-line @typescript-eslint/no-unused-vars
                const pusher = Pusher; // Needed for the Echo configuration to work
    
                echoInstance.current = 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,
                    forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
                    enabledTransports: ['ws', 'wss'],
                    authEndpoint: import.meta.env.VITE_AUTH_ENDPOINT,
                    auth: {
                        headers: {
                            'Authorization': 'Bearer ' + user?.access_token
                        }
                    }
                });
    
                // Listen for successful connection
                echoInstance.current.connector.pusher.connection.bind('connected', () => {
                    console.log('Successfully connected to Echo server.');
                });
    
                // Listen for connection errors
                echoInstance.current.connector.pusher.connection.bind('error', (error: any) => {
                    console.error('Error connecting to Echo server:', error);
                });
            }
    
            const channel = echoInstance.current.private('channel_for_everyone')
                .listen(CommentCreatedEvent', (event: any) => {
                    showNotificationToast({text: `You have received a new message from ${event?.user?.name}`);
                });
    
            // Cleanup function to remove listeners and Echo instance on unmount or dependency change
            return () => {
                if (echoInstance.current) {
                    channel.stopListening('CommentCreatedEvent');
                    echoInstance.current.disconnect();
                    echoInstance.current = null;
                }
            };
        }, [user, showNotificationToast]);
    
        return null;
    };