From b81fc392d7ede05078856326632dfb75f4588b28 Mon Sep 17 00:00:00 2001 From: Joseph Schultz Date: Fri, 26 Jul 2024 11:33:48 -0500 Subject: [PATCH] [Re:#167](https://github.com/supabase-community/supabase-csharp/issues/167) Adds support for specifying `GetHeaders` on the `RealtimeClient` which are included on the initial request to the server to establish websocket connection. --- Realtime/Client.cs | 24 ++++++++++++++++++++---- Realtime/ClientOptions.cs | 2 +- Realtime/Interfaces/IRealtimeClient.cs | 3 ++- Realtime/Interfaces/IRealtimeSocket.cs | 3 ++- Realtime/Realtime.csproj | 6 +++++- Realtime/RealtimeSocket.cs | 21 +++++++++++++++++++-- RealtimeTests/ClientTests.cs | 16 ++++++++++++++++ 7 files changed, 65 insertions(+), 10 deletions(-) diff --git a/Realtime/Client.cs b/Realtime/Client.cs index dff729c..1e54549 100644 --- a/Realtime/Client.cs +++ b/Realtime/Client.cs @@ -46,6 +46,21 @@ public class Client : IRealtimeClient /// public ClientOptions Options { get; } + private Func>? _getHeaders { get; set; } + + /// + public Func>? GetHeaders + { + get => _getHeaders; + set + { + _getHeaders = value; + + if (Socket != null) + Socket.GetHeaders = value; + } + } + /// /// Custom Serializer resolvers and converters that will be used for encoding and decoding Postgrest JSON responses. /// @@ -124,6 +139,7 @@ public async Task> ConnectAsync } Socket = new RealtimeSocket(_realtimeUrl, Options); + Socket.GetHeaders = GetHeaders; IRealtimeSocket.StateEventHandler? socketStateHandler = null; socketStateHandler = (sender, state) => @@ -172,7 +188,7 @@ public IRealtimeClient Connect( callback?.Invoke(this, null); return this; } - + Socket = new RealtimeSocket(_realtimeUrl, Options); IRealtimeSocket.StateEventHandler? socketStateHandler = null; IRealtimeSocket.ErrorEventHandler? errorEventHandler = null; @@ -183,12 +199,12 @@ public IRealtimeClient Connect( Socket.AddMessageReceivedHandler(HandleSocketMessageReceived); Socket.AddHeartbeatHandler(HandleSocketHeartbeat); - + sender.RemoveStateChangedHandler(socketStateHandler!); sender.RemoveErrorHandler(errorEventHandler!); - + NotifySocketStateChange(SocketState.Open); - + callback?.Invoke(this, null); }; diff --git a/Realtime/ClientOptions.cs b/Realtime/ClientOptions.cs index a2ea110..b5cf515 100644 --- a/Realtime/ClientOptions.cs +++ b/Realtime/ClientOptions.cs @@ -54,7 +54,7 @@ public class ClientOptions /// /// Request headers to be appended to the connection string. /// - public readonly Dictionary Headers = new(); + public readonly Dictionary Headers = new(); /// /// The optional params to pass when connecting diff --git a/Realtime/Interfaces/IRealtimeClient.cs b/Realtime/Interfaces/IRealtimeClient.cs index 37eaca3..a8f9cf7 100644 --- a/Realtime/Interfaces/IRealtimeClient.cs +++ b/Realtime/Interfaces/IRealtimeClient.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Net.WebSockets; using System.Threading.Tasks; +using Supabase.Core.Interfaces; using Supabase.Realtime.Exceptions; using static Supabase.Realtime.Constants; @@ -14,7 +15,7 @@ namespace Supabase.Realtime.Interfaces; /// /// /// -public interface IRealtimeClient +public interface IRealtimeClient: IGettableHeaders where TSocket : IRealtimeSocket where TChannel : IRealtimeChannel { diff --git a/Realtime/Interfaces/IRealtimeSocket.cs b/Realtime/Interfaces/IRealtimeSocket.cs index c107b0c..b0dfdc6 100644 --- a/Realtime/Interfaces/IRealtimeSocket.cs +++ b/Realtime/Interfaces/IRealtimeSocket.cs @@ -2,6 +2,7 @@ using System; using System.Net.WebSockets; using System.Threading.Tasks; +using Supabase.Core.Interfaces; using Supabase.Realtime.Exceptions; using static Supabase.Realtime.Constants; @@ -10,7 +11,7 @@ namespace Supabase.Realtime.Interfaces; /// /// Contract for a realtime socket. /// -public interface IRealtimeSocket +public interface IRealtimeSocket: IGettableHeaders { /// /// Is this socket connected? diff --git a/Realtime/Realtime.csproj b/Realtime/Realtime.csproj index c75ac93..04953f3 100644 --- a/Realtime/Realtime.csproj +++ b/Realtime/Realtime.csproj @@ -37,10 +37,14 @@ $(VersionPrefix) + + + + - + diff --git a/Realtime/RealtimeSocket.cs b/Realtime/RealtimeSocket.cs index 3d1dbdb..08aba29 100644 --- a/Realtime/RealtimeSocket.cs +++ b/Realtime/RealtimeSocket.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; +using Supabase.Core.Extensions; using Supabase.Realtime.Socket; using Supabase.Realtime.Exceptions; using Supabase.Realtime.Interfaces; @@ -15,7 +16,6 @@ #endif namespace Supabase.Realtime; - /// /// Socket connection handler. /// @@ -44,6 +44,15 @@ private string EndpointUrl } } + /// + public Func>? GetHeaders { get; set; } + + /// + /// Shortcut property that merges with + /// Headers specified in take precedence over + /// + internal Dictionary Headers => GetHeaders != null ? GetHeaders().MergeLeft(_options.Headers) : _options.Headers; + private readonly List _socketEventHandlers = new(); private readonly List _messageEventHandlers = new(); private readonly List _heartbeatEventHandlers = new(); @@ -76,7 +85,15 @@ public RealtimeSocket(string endpoint, ClientOptions options) if (!options.Headers.ContainsKey("X-Client-Info")) options.Headers.Add("X-Client-Info", Core.Util.GetAssemblyVersion(typeof(Client))); - _connection = new WebsocketClient(new Uri(EndpointUrl)); + _connection = new WebsocketClient(new Uri(EndpointUrl), () => + { + var socket = new ClientWebSocket(); + + foreach (var header in Headers) + socket.Options.SetRequestHeader(header.Key, header.Value); + + return socket; + }); } void IDisposable.Dispose() diff --git a/RealtimeTests/ClientTests.cs b/RealtimeTests/ClientTests.cs index 61fba5d..390e173 100644 --- a/RealtimeTests/ClientTests.cs +++ b/RealtimeTests/ClientTests.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Net; +using System.Net.Sockets; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using Supabase.Realtime.Exceptions; @@ -130,4 +132,18 @@ public async Task ClientCanReconnectAfterProgrammaticDisconnect() client!.Disconnect(); await client!.ConnectAsync(); } + + [TestMethod("Client: Sets headers")] + public async Task ClientCanSetHeaders() + { + client!.Disconnect(); + + client!.GetHeaders = () => new Dictionary() { { "testing", "123" } }; + await client.ConnectAsync(); + + Assert.IsNotNull(client!); + Assert.IsNotNull(client!.Socket); + Assert.IsNotNull(client!.Socket.GetHeaders); + Assert.AreEqual("123",client.Socket.GetHeaders()["testing"]); + } } \ No newline at end of file