diff --git a/src/LaunchDarkly.EventSource/EventSource.cs b/src/LaunchDarkly.EventSource/EventSource.cs index 34204cd..fa6e540 100644 --- a/src/LaunchDarkly.EventSource/EventSource.cs +++ b/src/LaunchDarkly.EventSource/EventSource.cs @@ -301,10 +301,10 @@ private async Task ConnectToEventSourceAsync(CancellationToken cancellationToken _eventDataUtf8ByteBuffer = null; var svc = GetEventSourceService(_configuration); - + svc.ConnectionOpened += (o, e) => { _lastSuccessfulConnectionTime = DateTime.Now; - SetReadyState(ReadyState.Open, OnOpened); + SetReadyState(ReadyState.Open, OnOpened, e.Headers); }; svc.ConnectionClosed += (o, e) => { SetReadyState(ReadyState.Closed, OnClosed); }; @@ -321,7 +321,7 @@ private void Close(ReadyState state) _logger.Debug("Close({0}) - state was {1}", state, ReadyState); SetReadyState(state, OnClosed); } - + private void ProcessResponseLineString(string content) { if (content == null) @@ -353,7 +353,8 @@ private void ProcessResponseLineUtf8(Utf8ByteSpan content) } - private void SetReadyState(ReadyState state, Action action = null) + private void SetReadyState(ReadyState state, Action action = null, + IEnumerable>> headers = null) { lock (this) { @@ -361,12 +362,13 @@ private void SetReadyState(ReadyState state, Action actio { return; } + _readyState = state; } if (action != null) { - action(new StateChangedEventArgs(state)); + action(new StateChangedEventArgs(state, headers)); } } diff --git a/src/LaunchDarkly.EventSource/EventSourceOpenedEventArgs.cs b/src/LaunchDarkly.EventSource/EventSourceOpenedEventArgs.cs new file mode 100644 index 0000000..04d7c06 --- /dev/null +++ b/src/LaunchDarkly.EventSource/EventSourceOpenedEventArgs.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace LaunchDarkly.EventSource +{ + /// + /// Event arguments for the EventSourceService opened event. + /// + internal class EventSourceOpenedEventArgs: EventArgs + { + /// + /// Construct new event arguments. + /// + /// headers from the HTTP response + public EventSourceOpenedEventArgs(IEnumerable>> headers) + { + Headers = headers; + } + + /// + /// Response headers from the underlying HTTP request. + /// + public IEnumerable>> Headers { get; } + } +} diff --git a/src/LaunchDarkly.EventSource/EventSourceService.cs b/src/LaunchDarkly.EventSource/EventSourceService.cs index 31c73f0..ac0b543 100644 --- a/src/LaunchDarkly.EventSource/EventSourceService.cs +++ b/src/LaunchDarkly.EventSource/EventSourceService.cs @@ -37,7 +37,7 @@ internal class EventSourceService /// /// Occurs when the connection to the EventSource API has been opened. /// - public event EventHandler ConnectionOpened; + public event EventHandler ConnectionOpened; /// /// Occurs when the connection to the EventSource API has been closed. /// @@ -108,7 +108,8 @@ CancellationToken cancellationToken throw new EventSourceServiceCancelledException( string.Format(Resources.ErrorWrongEncoding, encoding.HeaderName)); } - OnConnectionOpened(); + + OnConnectionOpened(response.Headers); if (_configuration.PreferDataAsUtf8Bytes) { @@ -257,9 +258,9 @@ private void ValidateResponse(HttpResponseMessage response) } } - private void OnConnectionOpened() + private void OnConnectionOpened(HttpHeaders headers) { - ConnectionOpened?.Invoke(this, EventArgs.Empty); + ConnectionOpened?.Invoke(this, new EventSourceOpenedEventArgs(headers)); } private void OnConnectionClosed() diff --git a/src/LaunchDarkly.EventSource/StateChangedEventArgs.cs b/src/LaunchDarkly.EventSource/StateChangedEventArgs.cs index 1f5a8b4..4d2074e 100644 --- a/src/LaunchDarkly.EventSource/StateChangedEventArgs.cs +++ b/src/LaunchDarkly.EventSource/StateChangedEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace LaunchDarkly.EventSource { @@ -16,13 +17,23 @@ public class StateChangedEventArgs : EventArgs /// public ReadyState ReadyState { get; } + /// + /// Get the response headers. Only populated for . + /// + /// A collection of header values when the ReadyState is Open, or null. + /// + /// + public IEnumerable>> Headers { get; } + /// /// Initializes a new instance of the class. /// /// One of the values, which represents the state of the EventSource connection. - public StateChangedEventArgs(ReadyState readyState) + /// Response headers when the is . Otherwise null. + public StateChangedEventArgs(ReadyState readyState, IEnumerable>> headers = null) { ReadyState = readyState; + Headers = headers; } } -} \ No newline at end of file +} diff --git a/test/LaunchDarkly.EventSource.Tests/EventSourceHttpBehaviorTest.cs b/test/LaunchDarkly.EventSource.Tests/EventSourceHttpBehaviorTest.cs index 0357ed0..5cd147b 100644 --- a/test/LaunchDarkly.EventSource.Tests/EventSourceHttpBehaviorTest.cs +++ b/test/LaunchDarkly.EventSource.Tests/EventSourceHttpBehaviorTest.cs @@ -165,6 +165,36 @@ public void HttpRequestModifier() } } + [Fact] + public async void OpenedEventIncludesHeaders() + { + var taskComplete = new TaskCompletionSource(); + + Handler streamHandler = Handlers.StartChunks("text/event-stream") + .Then(async ctx => + { + await Handlers.WriteChunkString("data:potato\n\n")(ctx); + await taskComplete.Task; + }); + using (var server = HttpServer.Start(streamHandler)) + { + using (var es = MakeEventSource(server.Uri)) + { + es.Opened += (sender, args) => + { + // The transfer-encoding header should always be present for an SSE stream. + taskComplete.SetResult( + args.Headers.FirstOrDefault(item => string.Equals(item.Key, "transfer-encoding", StringComparison.OrdinalIgnoreCase)).Value != null); + }; + _ = Task.Run(es.StartAsync); + + await taskComplete.Task; + } + } + + Assert.True(taskComplete.Task.Result); + } + [Fact] public void ReceiveEventStreamInChunks() {