Work around for a Firefox bug

Firefox won't fire EventSource open event until it receives some data. The workaround is to send an empty comment when starting ServerSentEvent transport.

Fixes: #352
This commit is contained in:
moozzyk 2017-04-20 09:50:53 -07:00 committed by Pawel Kadluczka
parent 39b2990b62
commit 82f99a1424
5 changed files with 94 additions and 44 deletions

View File

@ -14,26 +14,45 @@
</div>
<script src="lib/signalr-client/signalr-client.js"></script>
<script>
let transportType = signalR.TransportType[getParameterByName('transport')] || signalR.TransportType.WebSockets;
let connection = new signalR.HubConnection(`http://${document.location.host}/chat`, 'formatType=json&format=text');
connection.on('Send', function (message) {
var child = document.createElement('li');
child.innerText = message;
document.getElementById('messages').appendChild(child);
});
connection.on('Send', message => appendLine(message));
connection.onClosed = e => {
if (e) {
appendLine('Connection closed with error: ' + e, 'red');
}
else {
appendLine('Disconnected', 'green');
}
};
connection.start();
connection.start(transportType).catch(err => appendLine(err, 'red'));
document.getElementById('sendmessage').addEventListener('submit', event => {
let data = document.getElementById('new-message').value;
connection.invoke('Send', data).catch(err => {
var child = document.createElement('li');
child.style.color = 'red';
child.innerText = err;
document.getElementById('messages').appendChild(child);
});
connection.invoke('Send', data).catch(err => appendLine(err, 'red'));
event.preventDefault();
});
function appendLine(line, color) {
let child = document.createElement('li');
if (color) {
child.style.color = color;
}
child.innerText = line;
document.getElementById('messages').appendChild(child);
}
function getParameterByName(name, url) {
if (!url) {
url = window.location.href;
}
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
</script>

View File

@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
{
private const byte ByteCR = (byte)'\r';
private const byte ByteLF = (byte)'\n';
private const byte ByteColon = (byte)':';
private const byte ByteT = (byte)'T';
private const byte ByteB = (byte)'B';
private const byte ByteC = (byte)'C';
@ -38,7 +39,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
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>
@ -63,6 +63,14 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
throw new FormatException("There was an error in the frame format");
}
// Skip comments
if (line[0] == ByteColon)
{
start = lineEnd;
consumed = lineEnd;
continue;
}
if (IsMessageEnd(line))
{
_internalParserState = InternalParseState.ReadEndOfMessage;

View File

@ -37,6 +37,9 @@ namespace Microsoft.AspNetCore.Sockets.Transports
context.Response.Headers["Content-Encoding"] = "identity";
// Workaround for a Firefox bug where EventSource won't fire the open event
// until it receives some data
await context.Response.WriteAsync(":\r\n");
await context.Response.Body.FlushAsync();
var pipe = context.Response.Body.AsPipelineWriter();

View File

@ -16,14 +16,20 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
[Theory]
[InlineData("data: T\r\n\r\n", "", MessageType.Text)]
[InlineData("data: B\r\n\r\n", "", MessageType.Binary)]
[InlineData("data: T\r\n\r\n:\r\n", "", MessageType.Text)]
[InlineData("data: T\r\n\r\n:comment\r\n", "", MessageType.Text)]
[InlineData("data: T\r\ndata: \r\r\n\r\n", "\r", MessageType.Text)]
[InlineData("data: T\r\n:comment\r\ndata: \r\r\n\r\n", "\r", MessageType.Text)]
[InlineData("data: T\r\ndata: A\rB\r\n\r\n", "A\rB", MessageType.Text)]
[InlineData("data: T\r\ndata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)]
[InlineData("data: T\r\ndata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)]
[InlineData("data: T\r\ndata: Hello, World\r\n\r\ndata: ", "Hello, World", MessageType.Text)]
[InlineData("data: T\r\ndata: Hello, World\r\n\r\n:comment\r\ndata: ", "Hello, World", MessageType.Text)]
[InlineData("data: T\r\ndata: Hello, World\r\n\r\n:comment", "Hello, World", MessageType.Text)]
[InlineData("data: T\r\ndata: Hello, World\r\n\r\n:comment\r\n", "Hello, World", MessageType.Text)]
[InlineData("data: T\r\ndata: Hello, World\r\n:comment\r\n\r\n", "Hello, World", MessageType.Text)]
[InlineData("data: B\r\ndata: SGVsbG8sIFdvcmxk\r\n\r\n", "Hello, World", MessageType.Binary)]
[InlineData("data: B\r\ndata: SGVsbG8g\r\ndata: V29ybGQ=\r\n\r\n", "Hello World", MessageType.Binary)]
public void ParseSSEMessageSuccessCases(string encodedMessage, string expectedMessage, MessageType messageType)
public void ParseSSEMessageSuccessCases(string encodedMessage, string expectedMessage, MessageType messageType)
{
var buffer = Encoding.UTF8.GetBytes(encodedMessage);
var readableBuffer = ReadableBuffer.Create(buffer);
@ -68,6 +74,9 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
[Theory]
[InlineData("")]
[InlineData(":")]
[InlineData(":comment")]
[InlineData(":comment\r\n")]
[InlineData("data:")]
[InlineData("data: \r")]
[InlineData("data: T\r\nda")]
@ -77,6 +86,10 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
[InlineData("data: T\r\ndata: Hello, World\r\n")]
[InlineData("data: T\r\ndata: Hello, World\r\n\r")]
[InlineData("data: B\r\ndata: SGVsbG8sIFd")]
[InlineData(":\r\ndata:")]
[InlineData("data: T\r\n:\r\n")]
[InlineData("data: T\r\n:\r\ndata:")]
[InlineData("data: T\r\ndata: Hello, World\r\n:comment")]
public void ParseSSEMessageIncompleteParseResult(string encodedMessage)
{
var buffer = Encoding.UTF8.GetBytes(encodedMessage);
@ -89,40 +102,47 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
}
[Theory]
[InlineData("d", "ata: T\r\ndata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)]
[InlineData("data: T", "\r\ndata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)]
[InlineData("data: T\r", "\ndata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)]
[InlineData("data: T\r\n", "data: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)]
[InlineData("data: T\r\nd", "ata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)]
[InlineData("data: T\r\ndata: ", "Hello, World\r\n\r\n", "Hello, World", MessageType.Text)]
[InlineData("data: T\r\ndata: Hello, World", "\r\n\r\n", "Hello, World", MessageType.Text)]
[InlineData("data: T\r\ndata: Hello, World\r\n", "\r\n", "Hello, World", MessageType.Text)]
[InlineData("data: T", "\r\ndata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)]
[InlineData("data: ", "T\r\ndata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)]
[InlineData("data: B\r\ndata: SGVs", "bG8sIFdvcmxk\r\n\r\n", "Hello, World", MessageType.Binary)]
public async Task ParseMessageAcrossMultipleReadsSuccess(string encodedMessagePart1, string encodedMessagePart2, string expectedMessage, MessageType expectedMessageType)
[InlineData(new[] { "d", "ata: T\r\ndata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)]
[InlineData(new[] { "data: T", "\r\ndata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)]
[InlineData(new[] { "data: T\r", "\ndata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)]
[InlineData(new[] { "data: T\r\n", "data: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)]
[InlineData(new[] { "data: T\r\nd", "ata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)]
[InlineData(new[] { "data: T\r\ndata: ", "Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)]
[InlineData(new[] { "data: T\r\ndata: Hello, World", "\r\n\r\n" }, "Hello, World", MessageType.Text)]
[InlineData(new[] { "data: T\r\ndata: Hello, World\r\n", "\r\n" }, "Hello, World", MessageType.Text)]
[InlineData(new[] { "data: ", "T\r\ndata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)]
[InlineData(new[] { ":", "comment", "\r\n", "d", "ata: T\r\ndata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)]
[InlineData(new[] { "data: T\r\n", ":comment", "\r\n", "data: Hello, World", "\r\n\r\n" }, "Hello, World", MessageType.Text)]
[InlineData(new[] { "data: T\r\ndata: Hello, World\r\n", ":comment\r\n", "\r\n" }, "Hello, World", MessageType.Text)]
[InlineData(new[] { "data: B\r\ndata: SGVs", "bG8sIFdvcmxk\r\n\r\n" }, "Hello, World", MessageType.Binary)]
public async Task ParseMessageAcrossMultipleReadsSuccess(string[] messageParts, string expectedMessage, MessageType expectedMessageType)
{
using (var pipeFactory = new PipeFactory())
{
var parser = new ServerSentEventsMessageParser();
var pipe = pipeFactory.Create();
// Read the first part of the message
await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(encodedMessagePart1));
Message message = default(Message);
ReadCursor consumed = default(ReadCursor), examined = default(ReadCursor);
var result = await pipe.Reader.ReadAsync();
var parser = new ServerSentEventsMessageParser();
for (var i = 0; i < messageParts.Length; i++)
{
var messagePart = messageParts[i];
await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(messagePart));
var result = await pipe.Reader.ReadAsync();
var parseResult = parser.ParseMessage(result.Buffer, out var consumed, out var examined, out Message message);
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Incomplete, parseResult);
var parseResult = parser.ParseMessage(result.Buffer, out consumed, out examined, out message);
pipe.Reader.Advance(consumed, examined);
pipe.Reader.Advance(consumed, examined);
// parse result should be complete only after we parsed the last message part
var expectedResult =
i == messageParts.Length - 1
? ServerSentEventsMessageParser.ParseResult.Completed
: ServerSentEventsMessageParser.ParseResult.Incomplete;
// Send the rest of the data and parse the complete message
await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(encodedMessagePart2));
result = await pipe.Reader.ReadAsync();
Assert.Equal(expectedResult, parseResult);
}
parseResult = parser.ParseMessage(result.Buffer, out consumed, out examined, out message);
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Completed, parseResult);
Assert.Equal(expectedMessageType, message.Type);
Assert.Equal(consumed, examined);

View File

@ -49,9 +49,9 @@ namespace Microsoft.AspNetCore.Sockets.Tests
}
[Theory]
[InlineData("Hello World", "data: T\r\ndata: Hello World\r\n\r\n")]
[InlineData("Hello\nWorld", "data: T\r\ndata: Hello\r\ndata: World\r\n\r\n")]
[InlineData("Hello\r\nWorld", "data: T\r\ndata: Hello\r\ndata: World\r\n\r\n")]
[InlineData("Hello World", ":\r\ndata: T\r\ndata: Hello World\r\n\r\n")]
[InlineData("Hello\nWorld", ":\r\ndata: T\r\ndata: Hello\r\ndata: World\r\n\r\n")]
[InlineData("Hello\r\nWorld", ":\r\ndata: T\r\ndata: Hello\r\ndata: World\r\n\r\n")]
public async Task SSEAddsAppropriateFraming(string message, string expected)
{
var channel = Channel.CreateUnbounded<Message>();