Server-Sent Events Transport + Parser (#401)

This commit is contained in:
Mikael Mengistu 2017-04-18 16:08:48 -07:00 committed by GitHub
parent 2d278009b2
commit 8c8f6c708b
6 changed files with 702 additions and 12 deletions

View File

@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
if ((availableServerTransports & TransportType.ServerSentEvents & _requestedTransportType) == TransportType.ServerSentEvents)
{
throw new NotImplementedException();
return new ServerSentEventsTransport(_httpClient, _loggerFactory);
}
if ((availableServerTransports & TransportType.LongPolling & _requestedTransportType) == TransportType.LongPolling)

View File

@ -0,0 +1,231 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.IO.Pipelines.Text.Primitives;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Formatting;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Sockets.Client
{
public class ServerSentEventsTransport : ITransport
{
private static readonly string DefaultUserAgent = "Microsoft.AspNetCore.SignalR.Client/0.0.0";
private static readonly ProductInfoHeaderValue DefaultUserAgentHeader = ProductInfoHeaderValue.Parse(DefaultUserAgent);
private readonly HttpClient _httpClient;
private readonly ILogger _logger;
private readonly CancellationTokenSource _transportCts = new CancellationTokenSource();
private readonly ServerSentEventsMessageParser _parser = new ServerSentEventsMessageParser();
private IChannelConnection<SendMessage, Message> _application;
public Task Running { get; private set; } = Task.CompletedTask;
public ServerSentEventsTransport(HttpClient httpClient)
: this(httpClient, null)
{ }
public ServerSentEventsTransport(HttpClient httpClient, ILoggerFactory loggerFactory)
{
if (httpClient == null)
{
throw new ArgumentNullException(nameof(_httpClient));
}
_httpClient = httpClient;
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<ServerSentEventsTransport>();
}
public Task StartAsync(Uri url, IChannelConnection<SendMessage, Message> application)
{
_logger.LogInformation("Starting {transportName}", nameof(ServerSentEventsTransport));
_application = application;
var sseUrl = Utils.AppendPath(url, "sse");
var sendUrl = Utils.AppendPath(url, "send");
var sendTask = SendMessages(sendUrl, _transportCts.Token);
var receiveTask = OpenConnection(_application, sseUrl, _transportCts.Token);
Running = Task.WhenAll(sendTask, receiveTask).ContinueWith(t =>
{
if (t.Exception != null) { _logger.LogError(t.Exception, "Transport stopped"); }
_application.Output.TryComplete(t.IsFaulted ? t.Exception.InnerException : null);
return t;
}).Unwrap();
return TaskCache.CompletedTask;
}
private async Task OpenConnection(IChannelConnection<SendMessage, Message> application, Uri url, CancellationToken cancellationToken)
{
_logger.LogInformation("Starting receive loop");
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
var stream = await response.Content.ReadAsStreamAsync();
var pipelineReader = stream.AsPipelineReader();
try
{
while (true)
{
var result = await pipelineReader.ReadAsync();
var input = result.Buffer;
var consumed = input.Start;
var examined = input.End;
try
{
if (input.IsEmpty && result.IsCompleted)
{
_logger.LogDebug("Server-Sent Event Stream ended");
break;
}
var parseResult = _parser.ParseMessage(input, out consumed, out examined, out var message);
switch (parseResult)
{
case ServerSentEventsMessageParser.ParseResult.Completed:
_application.Output.TryWrite(message);
_parser.Reset();
break;
case ServerSentEventsMessageParser.ParseResult.Incomplete:
if (result.IsCompleted)
{
throw new FormatException("Incomplete message");
}
break;
}
}
finally
{
pipelineReader.Advance(consumed, examined);
}
}
}
finally
{
_transportCts.Cancel();
}
}
private async Task SendMessages(Uri sendUrl, CancellationToken cancellationToken)
{
_logger.LogInformation("Starting the send loop");
List<SendMessage> messages = null;
try
{
while (await _application.Input.WaitToReadAsync(cancellationToken))
{
messages = new List<SendMessage>();
while (!cancellationToken.IsCancellationRequested && _application.Input.TryRead(out SendMessage message))
{
messages.Add(message);
}
if (messages.Count > 0)
{
_logger.LogDebug("Sending {messageCount} message(s) to the server using url: {url}", messages.Count, sendUrl);
var request = new HttpRequestMessage(HttpMethod.Post, sendUrl);
request.Headers.UserAgent.Add(DefaultUserAgentHeader);
var memoryStream = new MemoryStream();
var pipe = memoryStream.AsPipelineWriter();
var output = new PipelineTextOutput(pipe, TextEncoder.Utf8);
await WriteMessagesAsync(messages, output, MessageFormat.Binary);
memoryStream.Seek(0, SeekOrigin.Begin);
request.Content = new StreamContent(memoryStream);
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(MessageFormatter.GetContentType(MessageFormat.Binary));
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
_logger.LogDebug("Message(s) sent successfully");
foreach (var message in messages)
{
message.SendResult?.TrySetResult(null);
}
}
}
}
catch (OperationCanceledException)
{
_logger.LogError("Send cancelled");
if (messages != null)
{
foreach (var message in messages)
{
message.SendResult?.TrySetCanceled();
}
}
}
catch (Exception ex)
{
_logger.LogDebug("Error while sending to '{url}' : '{exception}'", sendUrl, ex);
if (messages != null)
{
foreach (var message in messages)
{
message.SendResult?.TrySetException(ex);
}
}
throw;
}
finally
{
// Make sure the poll loop is terminated
_transportCts.Cancel();
}
_logger.LogInformation("Send loop stopped");
}
private async Task WriteMessagesAsync(List<SendMessage> messages, PipelineTextOutput output, MessageFormat format)
{
output.Append(MessageFormatter.GetFormatIndicator(format), TextEncoder.Utf8);
foreach (var message in messages)
{
_logger.LogDebug("Writing '{messageType}' message to the server", message.Type);
var payload = message.Payload ?? Array.Empty<byte>();
if (!MessageFormatter.TryWriteMessage(new Message(payload, message.Type, endOfMessage: true), output, format))
{
// We didn't get any more memory!
throw new InvalidOperationException("Unable to write message to pipeline");
}
await output.FlushAsync();
}
}
public async Task StopAsync()
{
_logger.LogInformation("Transport {transportName} is stopping", nameof(ServerSentEventsTransport));
_transportCts.Cancel();
_application.Output.TryComplete();
await Running;
}
}
}

View File

@ -0,0 +1,215 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO.Pipelines;
using System.Runtime.CompilerServices;
using System.Text;
namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
{
public class ServerSentEventsMessageParser
{
private const byte ByteCR = (byte)'\r';
private const byte ByteLF = (byte)'\n';
private const byte ByteT = (byte)'T';
private const byte ByteB = (byte)'B';
private const byte ByteC = (byte)'C';
private const byte ByteE = (byte)'E';
private static byte[] _dataPrefix = Encoding.UTF8.GetBytes("data: ");
private static byte[] _sseLineEnding = Encoding.UTF8.GetBytes("\r\n");
private static byte[] _newLine = Encoding.UTF8.GetBytes(Environment.NewLine);
private InternalParseState _internalParserState = InternalParseState.ReadMessageType;
private List<byte[]> _data = new List<byte[]>();
private MessageType _messageType = MessageType.Text;
public ParseResult ParseMessage(ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out Message message)
{
consumed = buffer.Start;
examined = buffer.End;
message = new Message();
var reader = new ReadableBufferReader(buffer);
_messageType = MessageType.Text;
var start = consumed;
var end = examined;
while (!reader.End)
{
if (ReadCursorOperations.Seek(start, end, out var lineEnd, ByteLF) == -1)
{
// For the case of data: Foo\r\n\r\<Anytine except \n>
if (_internalParserState == InternalParseState.ReadEndOfMessage)
{
if(ConvertBufferToSpan(buffer.Slice(start, buffer.End)).Length > 1)
{
throw new FormatException("Expected a \\r\\n frame ending");
}
}
// Partial message. We need to read more.
return ParseResult.Incomplete;
}
lineEnd = buffer.Move(lineEnd, 1);
var line = ConvertBufferToSpan(buffer.Slice(start, lineEnd));
reader.Skip(line.Length);
if (line.Length <= 1)
{
throw new FormatException("There was an error in the frame format");
}
if (IsMessageEnd(line))
{
_internalParserState = InternalParseState.ReadEndOfMessage;
}
// To ensure that the \n was preceded by a \r
// since messages can't contain \n.
// data: foo\n\bar should be encoded as
// data: foo\r\n
// data: bar\r\n
else if (line[line.Length - _sseLineEnding.Length] != ByteCR)
{
throw new FormatException("Unexpected '\n' in message. A '\n' character can only be used as part of the newline sequence '\r\n'");
}
else
{
EnsureStartsWithDataPrefix(line);
}
switch (_internalParserState)
{
case InternalParseState.ReadMessageType:
_messageType = GetMessageType(line);
_internalParserState = InternalParseState.ReadMessagePayload;
start = lineEnd;
consumed = lineEnd;
break;
case InternalParseState.ReadMessagePayload:
// Slice away the 'data: '
var payloadLength = line.Length - (_dataPrefix.Length + _sseLineEnding.Length);
var newData = line.Slice(_dataPrefix.Length, payloadLength).ToArray();
_data.Add(newData);
start = lineEnd;
consumed = lineEnd;
break;
case InternalParseState.ReadEndOfMessage:
if (_data.Count > 0)
{
// Find the final size of the payload
var payloadSize = 0;
foreach (var dataLine in _data)
{
payloadSize += dataLine.Length + _newLine.Length;
}
// Allocate space in the paylod buffer for the data and the new lines.
// Subtract newLine length because we don't want a trailing newline.
var payload = new byte[payloadSize - _newLine.Length];
var offset = 0;
foreach (var dataLine in _data)
{
dataLine.CopyTo(payload, offset);
offset += dataLine.Length;
if (offset < payload.Length)
{
_newLine.CopyTo(payload, offset);
offset += _newLine.Length;
}
}
message = new Message(payload, _messageType);
}
else
{
// Empty message
message = new Message(Array.Empty<byte>(), _messageType);
}
consumed = lineEnd;
examined = consumed;
return ParseResult.Completed;
}
if (reader.Peek() == ByteCR)
{
_internalParserState = InternalParseState.ReadEndOfMessage;
}
}
return ParseResult.Incomplete;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Span<byte> ConvertBufferToSpan(ReadableBuffer buffer)
{
if (buffer.IsSingleSpan)
{
return buffer.First.Span;
}
return buffer.ToArray();
}
public void Reset()
{
_internalParserState = InternalParseState.ReadMessageType;
_data.Clear();
}
private void EnsureStartsWithDataPrefix(ReadOnlySpan<byte> line)
{
if (!line.StartsWith(_dataPrefix))
{
throw new FormatException("Expected the message prefix 'data: '");
}
}
private bool IsMessageEnd(ReadOnlySpan<byte> line)
{
return line.Length == _sseLineEnding.Length && line.SequenceEqual(_sseLineEnding);
}
private MessageType GetMessageType(ReadOnlySpan<byte> line)
{
EnsureStartsWithDataPrefix(line);
// Skip the "data: " part of the line
var type = line[_dataPrefix.Length];
switch (type)
{
case ByteT:
return MessageType.Text;
case ByteB:
throw new NotSupportedException("Support for binary messages has not been implemented yet");
case ByteC:
return MessageType.Close;
case ByteE:
return MessageType.Error;
default:
throw new FormatException($"Unknown message type: '{(char)type}'");
}
}
public enum ParseResult
{
Completed,
Incomplete,
}
private enum InternalParseState
{
ReadMessageType,
ReadMessagePayload,
ReadEndOfMessage,
Error
}
}
}

View File

@ -33,6 +33,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
[Theory]
[InlineData(TransportType.WebSockets, typeof(WebSocketsTransport))]
[InlineData(TransportType.ServerSentEvents, typeof(ServerSentEventsTransport))]
[InlineData(TransportType.LongPolling, typeof(LongPollingTransport))]
[OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2, SkipReason = "No WebSockets Client for this platform")]
public void DefaultTransportFactoryCreatesRequestedTransportIfAvailable(TransportType requestedTransport, Type expectedTransportType)
@ -67,7 +68,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
}
[Theory]
[InlineData(TransportType.WebSockets, typeof(LongPollingTransport))]
[InlineData(TransportType.WebSockets, typeof(ServerSentEventsTransport))]
[InlineData(TransportType.LongPolling, typeof(LongPollingTransport))]
public void DefaultTransportFactoryCreatesRequestedTransportIfAvailable_Win7(TransportType requestedTransport, Type expectedTransportType)
{

View File

@ -78,16 +78,16 @@ namespace Microsoft.AspNetCore.SignalR.Tests
var receiveTcs = new TaskCompletionSource<string>();
connection.Received += (data, format) => receiveTcs.TrySetResult(Encoding.UTF8.GetString(data));
connection.Closed += e =>
{
if (e != null)
{
if (e != null)
{
receiveTcs.TrySetException(e);
}
else
{
receiveTcs.TrySetResult(null);
}
};
receiveTcs.TrySetException(e);
}
else
{
receiveTcs.TrySetResult(null);
}
};
await connection.StartAsync(transportType);
@ -146,7 +146,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests
new[]
{
new object[] { TransportType.WebSockets },
new object[] { TransportType.ServerSentEvents },
new object[] { TransportType.LongPolling }
};
}
}
}

View File

@ -0,0 +1,242 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO.Pipelines;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
using Xunit;
namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
{
public class ServerSentEventsParserTests
{
[Theory]
[InlineData("data: T\r\n\r\n", "")]
[InlineData("data: T\r\ndata: \r\r\n\r\n", "\r")]
[InlineData("data: T\r\ndata: A\rB\r\n\r\n", "A\rB")]
[InlineData("data: T\r\ndata: Hello, World\r\n\r\n", "Hello, World")]
[InlineData("data: T\r\ndata: Hello, World\r\n\r\n", "Hello, World")]
[InlineData("data: T\r\ndata: Hello, World\r\n\r\ndata: ", "Hello, World")]
public void ParseSSEMessageSuccessCases(string encodedMessage, string expectedMessage)
{
var buffer = Encoding.UTF8.GetBytes(encodedMessage);
var readableBuffer = ReadableBuffer.Create(buffer);
var parser = new ServerSentEventsMessageParser();
var parseResult = parser.ParseMessage(readableBuffer, out var consumed, out var examined, out Message message);
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Completed, parseResult);
Assert.Equal(MessageType.Text, message.Type);
Assert.Equal(consumed, examined);
var result = Encoding.UTF8.GetString(message.Payload);
Assert.Equal(expectedMessage, result);
}
[Theory]
[InlineData("data: X\r\n", "Unknown message type: 'X'")]
[InlineData("data: T\n", "Unexpected '\n' in message. A '\n' character can only be used as part of the newline sequence '\r\n'")]
[InlineData("data: X\r\n\r\n", "Unknown message type: 'X'")]
[InlineData("data: Not the message type\r\n\r\n", "Unknown message type: 'N'")]
[InlineData("data: T\r\ndata: Hello, World\r\r\n\n", "There was an error in the frame format")]
[InlineData("data: Not the message type\r\r\n", "Unknown message type: 'N'")]
[InlineData("data: T\r\ndata: Hello, World\n\n", "Unexpected '\n' in message. A '\n' character can only be used as part of the newline sequence '\r\n'")]
[InlineData("data: T\r\nfoo: Hello, World\r\n\r\n", "Expected the message prefix 'data: '")]
[InlineData("foo: T\r\ndata: Hello, World\r\n\r\n", "Expected the message prefix 'data: '")]
[InlineData("food: T\r\ndata: Hello, World\r\n\r\n", "Expected the message prefix 'data: '")]
[InlineData("data: T\r\ndata: Hello, World\r\n\n", "There was an error in the frame format")]
[InlineData("data: T\r\ndata: Hello\n, World\r\n\r\n", "Unexpected '\n' in message. A '\n' character can only be used as part of the newline sequence '\r\n'")]
[InlineData("data: data: \r\n", "Unknown message type: 'd'")]
[InlineData("data: T\r\ndata: Hello, World\r\n\r\\", "Expected a \\r\\n frame ending")]
[InlineData("data: T\r\ndata: Major\r\ndata: Key\rndata: Alert\r\n\r\\", "Expected a \\r\\n frame ending")]
[InlineData("data: T\r\ndata: Major\r\ndata: Key\r\ndata: Alert\r\n\r\\", "Expected a \\r\\n frame ending")]
public void ParseSSEMessageFailureCases(string encodedMessage, string expectedExceptionMessage)
{
var buffer = Encoding.UTF8.GetBytes(encodedMessage);
var readableBuffer = ReadableBuffer.Create(buffer);
var parser = new ServerSentEventsMessageParser();
var ex = Assert.Throws<FormatException>(() => { parser.ParseMessage(readableBuffer, out var consumed, out var examined, out Message message); });
Assert.Equal(expectedExceptionMessage, ex.Message);
}
[Theory]
[InlineData("")]
[InlineData("data:")]
[InlineData("data: \r")]
[InlineData("data: T\r\nda")]
[InlineData("data: T\r\ndata:")]
[InlineData("data: T\r\ndata: Hello, World")]
[InlineData("data: T\r\ndata: Hello, World\r")]
[InlineData("data: T\r\ndata: Hello, World\r\n")]
[InlineData("data: T\r\ndata: Hello, World\r\n\r")]
public void ParseSSEMessageIncompleteParseResult(string encodedMessage)
{
var buffer = Encoding.UTF8.GetBytes(encodedMessage);
var readableBuffer = ReadableBuffer.Create(buffer);
var parser = new ServerSentEventsMessageParser();
var parseResult = parser.ParseMessage(readableBuffer, out var consumed, out var examined, out Message message);
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Incomplete, parseResult);
}
[Theory]
[InlineData("d", "ata: T\r\ndata: Hello, World\r\n\r\n", "Hello, World")]
[InlineData("data: T", "\r\ndata: Hello, World\r\n\r\n", "Hello, World")]
[InlineData("data: T\r", "\ndata: Hello, World\r\n\r\n", "Hello, World")]
[InlineData("data: T\r\n", "data: Hello, World\r\n\r\n", "Hello, World")]
[InlineData("data: T\r\nd", "ata: Hello, World\r\n\r\n", "Hello, World")]
[InlineData("data: T\r\ndata: ", "Hello, World\r\n\r\n", "Hello, World")]
[InlineData("data: T\r\ndata: Hello, World", "\r\n\r\n", "Hello, World")]
[InlineData("data: T\r\ndata: Hello, World\r\n", "\r\n", "Hello, World")]
[InlineData("data: T", "\r\ndata: Hello, World\r\n\r\n", "Hello, World")]
[InlineData("data: ", "T\r\ndata: Hello, World\r\n\r\n", "Hello, World")]
public async Task ParseMessageAcrossMultipleReadsSuccess(string encodedMessagePart1, string encodedMessagePart2, string expectedMessage)
{
using (var pipeFactory = new PipeFactory())
{
var pipe = pipeFactory.Create();
// Read the first part of the message
await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(encodedMessagePart1));
var result = await pipe.Reader.ReadAsync();
var parser = new ServerSentEventsMessageParser();
var parseResult = parser.ParseMessage(result.Buffer, out var consumed, out var examined, out Message message);
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Incomplete, parseResult);
pipe.Reader.Advance(consumed, examined);
// Send the rest of the data and parse the complete message
await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(encodedMessagePart2));
result = await pipe.Reader.ReadAsync();
parseResult = parser.ParseMessage(result.Buffer, out consumed, out examined, out message);
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Completed, parseResult);
Assert.Equal(MessageType.Text, message.Type);
Assert.Equal(consumed, examined);
var resultMessage = Encoding.UTF8.GetString(message.Payload);
Assert.Equal(expectedMessage, resultMessage);
}
}
[Theory]
[InlineData("data: ", "X\r\n", "Unknown message type: 'X'")]
[InlineData("data: T", "\n", "Unexpected '\n' in message. A '\n' character can only be used as part of the newline sequence '\r\n'")]
[InlineData("data: ", "X\r\n\r\n", "Unknown message type: 'X'")]
[InlineData("data: ", "Not the message type\r\n\r\n", "Unknown message type: 'N'")]
[InlineData("data: T\r\n", "data: Hello, World\r\r\n\n", "There was an error in the frame format")]
[InlineData("data:", " Not the message type\r\r\n", "Unknown message type: 'N'")]
[InlineData("data: T\r\n", "data: Hello, World\n\n", "Unexpected '\n' in message. A '\n' character can only be used as part of the newline sequence '\r\n'")]
[InlineData("data: T\r\nf", "oo: Hello, World\r\n\r\n", "Expected the message prefix 'data: '")]
[InlineData("foo", ": T\r\ndata: Hello, World\r\n\r\n", "Expected the message prefix 'data: '")]
[InlineData("food:", " T\r\ndata: Hello, World\r\n\r\n", "Expected the message prefix 'data: '")]
[InlineData("data: T\r\ndata: Hello, W", "orld\r\n\n", "There was an error in the frame format")]
[InlineData("data: T\r\nda", "ta: Hello\n, World\r\n\r\n", "Unexpected '\n' in message. A '\n' character can only be used as part of the newline sequence '\r\n'")]
[InlineData("data:", " data: \r\n", "Unknown message type: 'd'")]
[InlineData("data: ", "T\r\ndata: Major\r\ndata: Key\r\ndata: Alert\r\n\r\\", "Expected a \\r\\n frame ending")]
public async Task ParseMessageAcrossMultipleReadsFailure(string encodedMessagePart1, string encodedMessagePart2, string expectedMessage)
{
using (var pipeFactory = new PipeFactory())
{
var pipe = pipeFactory.Create();
// Read the first part of the message
await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(encodedMessagePart1));
var result = await pipe.Reader.ReadAsync();
var parser = new ServerSentEventsMessageParser();
var parseResult = parser.ParseMessage(result.Buffer, out var consumed, out var examined, out Message message);
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Incomplete, parseResult);
pipe.Reader.Advance(consumed, examined);
// Send the rest of the data and parse the complete message
await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(encodedMessagePart2));
result = await pipe.Reader.ReadAsync();
var ex = Assert.Throws<FormatException>(() => parser.ParseMessage(result.Buffer, out consumed, out examined, out message));
Assert.Equal(expectedMessage, ex.Message);
}
}
[Fact]
public async Task ParseMultipleMessages()
{
using (var pipeFactory = new PipeFactory())
{
var pipe = pipeFactory.Create();
var message1 = "data: T\r\ndata: foo\r\n\r\n";
var message2 = "data: T\r\ndata: bar\r\n\r\n";
// Read the first part of the message
await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(message1 + message2));
var result = await pipe.Reader.ReadAsync();
var parser = new ServerSentEventsMessageParser();
var parseResult = parser.ParseMessage(result.Buffer, out var consumed, out var examined, out var message);
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Completed, parseResult);
Assert.Equal(MessageType.Text, message.Type);
Assert.Equal("foo", Encoding.UTF8.GetString(message.Payload));
Assert.Equal(consumed, result.Buffer.Move(result.Buffer.Start, message1.Length));
pipe.Reader.Advance(consumed, examined);
Assert.Equal(consumed, examined);
parser.Reset();
result = await pipe.Reader.ReadAsync();
parseResult = parser.ParseMessage(result.Buffer, out consumed, out examined, out message);
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Completed, parseResult);
Assert.Equal(MessageType.Text, message.Type);
Assert.Equal("bar", Encoding.UTF8.GetString(message.Payload));
pipe.Reader.Advance(consumed, examined);
}
}
public static IEnumerable<object[]> MultilineMessages
{
get
{
yield return new object[] { "data: T\r\ndata: Shaolin\r\ndata: Fantastic\r\n\r\n", "Shaolin" + Environment.NewLine + " Fantastic" };
yield return new object[] { "data: T\r\ndata: The\r\ndata: Get\r\ndata: Down\r\n\r\n", "The" + Environment.NewLine + "Get" + Environment.NewLine + "Down" };
}
}
[Theory]
[MemberData(nameof(MultilineMessages))]
public void ParseMessagesWithMultipleDataLines(string encodedMessage, string expectedMessage)
{
var buffer = Encoding.UTF8.GetBytes(encodedMessage);
var readableBuffer = ReadableBuffer.Create(buffer);
var parser = new ServerSentEventsMessageParser();
var parseResult = parser.ParseMessage(readableBuffer, out var consumed, out var examined, out Message message);
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Completed, parseResult);
Assert.Equal(MessageType.Text, message.Type);
Assert.Equal(consumed, examined);
var result = Encoding.UTF8.GetString(message.Payload);
Assert.Equal(expectedMessage, result);
}
[Fact]
public void ParseSSEMessageBinaryNotSupported()
{
var encodedMessage = "data: B\r\ndata: \r\n\r\n";
var buffer = Encoding.UTF8.GetBytes(encodedMessage);
var readableBuffer = ReadableBuffer.Create(buffer);
var parser = new ServerSentEventsMessageParser();
var ex = Assert.Throws<NotSupportedException>(() => { parser.ParseMessage(readableBuffer, out var consumed, out var examined, out Message message); });
Assert.Equal("Support for binary messages has not been implemented yet", ex.Message);
}
}
}