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:
parent
39b2990b62
commit
82f99a1424
|
|
@ -14,26 +14,45 @@
|
||||||
</div>
|
</div>
|
||||||
<script src="lib/signalr-client/signalr-client.js"></script>
|
<script src="lib/signalr-client/signalr-client.js"></script>
|
||||||
<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');
|
let connection = new signalR.HubConnection(`http://${document.location.host}/chat`, 'formatType=json&format=text');
|
||||||
|
|
||||||
connection.on('Send', function (message) {
|
connection.on('Send', message => appendLine(message));
|
||||||
var child = document.createElement('li');
|
connection.onClosed = e => {
|
||||||
child.innerText = message;
|
if (e) {
|
||||||
document.getElementById('messages').appendChild(child);
|
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 => {
|
document.getElementById('sendmessage').addEventListener('submit', event => {
|
||||||
let data = document.getElementById('new-message').value;
|
let data = document.getElementById('new-message').value;
|
||||||
|
connection.invoke('Send', data).catch(err => appendLine(err, 'red'));
|
||||||
connection.invoke('Send', data).catch(err => {
|
|
||||||
var child = document.createElement('li');
|
|
||||||
child.style.color = 'red';
|
|
||||||
child.innerText = err;
|
|
||||||
document.getElementById('messages').appendChild(child);
|
|
||||||
});
|
|
||||||
|
|
||||||
event.preventDefault();
|
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>
|
</script>
|
||||||
|
|
@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
||||||
{
|
{
|
||||||
private const byte ByteCR = (byte)'\r';
|
private const byte ByteCR = (byte)'\r';
|
||||||
private const byte ByteLF = (byte)'\n';
|
private const byte ByteLF = (byte)'\n';
|
||||||
|
private const byte ByteColon = (byte)':';
|
||||||
private const byte ByteT = (byte)'T';
|
private const byte ByteT = (byte)'T';
|
||||||
private const byte ByteB = (byte)'B';
|
private const byte ByteB = (byte)'B';
|
||||||
private const byte ByteC = (byte)'C';
|
private const byte ByteC = (byte)'C';
|
||||||
|
|
@ -38,7 +39,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
|
||||||
|
|
||||||
while (!reader.End)
|
while (!reader.End)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (ReadCursorOperations.Seek(start, end, out var lineEnd, ByteLF) == -1)
|
if (ReadCursorOperations.Seek(start, end, out var lineEnd, ByteLF) == -1)
|
||||||
{
|
{
|
||||||
// For the case of data: Foo\r\n\r\<Anytine except \n>
|
// 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");
|
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))
|
if (IsMessageEnd(line))
|
||||||
{
|
{
|
||||||
_internalParserState = InternalParseState.ReadEndOfMessage;
|
_internalParserState = InternalParseState.ReadEndOfMessage;
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,9 @@ namespace Microsoft.AspNetCore.Sockets.Transports
|
||||||
|
|
||||||
context.Response.Headers["Content-Encoding"] = "identity";
|
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();
|
await context.Response.Body.FlushAsync();
|
||||||
|
|
||||||
var pipe = context.Response.Body.AsPipelineWriter();
|
var pipe = context.Response.Body.AsPipelineWriter();
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,20 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("data: T\r\n\r\n", "", MessageType.Text)]
|
[InlineData("data: T\r\n\r\n", "", MessageType.Text)]
|
||||||
[InlineData("data: B\r\n\r\n", "", MessageType.Binary)]
|
[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\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: 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\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\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: 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)]
|
[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 buffer = Encoding.UTF8.GetBytes(encodedMessage);
|
||||||
var readableBuffer = ReadableBuffer.Create(buffer);
|
var readableBuffer = ReadableBuffer.Create(buffer);
|
||||||
|
|
@ -68,6 +74,9 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("")]
|
[InlineData("")]
|
||||||
|
[InlineData(":")]
|
||||||
|
[InlineData(":comment")]
|
||||||
|
[InlineData(":comment\r\n")]
|
||||||
[InlineData("data:")]
|
[InlineData("data:")]
|
||||||
[InlineData("data: \r")]
|
[InlineData("data: \r")]
|
||||||
[InlineData("data: T\r\nda")]
|
[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")]
|
||||||
[InlineData("data: T\r\ndata: Hello, World\r\n\r")]
|
[InlineData("data: T\r\ndata: Hello, World\r\n\r")]
|
||||||
[InlineData("data: B\r\ndata: SGVsbG8sIFd")]
|
[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)
|
public void ParseSSEMessageIncompleteParseResult(string encodedMessage)
|
||||||
{
|
{
|
||||||
var buffer = Encoding.UTF8.GetBytes(encodedMessage);
|
var buffer = Encoding.UTF8.GetBytes(encodedMessage);
|
||||||
|
|
@ -89,40 +102,47 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("d", "ata: T\r\ndata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)]
|
[InlineData(new[] { "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(new[] { "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(new[] { "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(new[] { "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(new[] { "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(new[] { "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(new[] { "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(new[] { "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(new[] { "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(new[] { ":", "comment", "\r\n", "d", "ata: 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)]
|
[InlineData(new[] { "data: T\r\n", ":comment", "\r\n", "data: Hello, World", "\r\n\r\n" }, "Hello, World", MessageType.Text)]
|
||||||
public async Task ParseMessageAcrossMultipleReadsSuccess(string encodedMessagePart1, string encodedMessagePart2, string expectedMessage, MessageType expectedMessageType)
|
[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())
|
using (var pipeFactory = new PipeFactory())
|
||||||
{
|
{
|
||||||
|
var parser = new ServerSentEventsMessageParser();
|
||||||
var pipe = pipeFactory.Create();
|
var pipe = pipeFactory.Create();
|
||||||
|
|
||||||
// Read the first part of the message
|
Message message = default(Message);
|
||||||
await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(encodedMessagePart1));
|
ReadCursor consumed = default(ReadCursor), examined = default(ReadCursor);
|
||||||
|
|
||||||
var result = await pipe.Reader.ReadAsync();
|
for (var i = 0; i < messageParts.Length; i++)
|
||||||
var parser = new ServerSentEventsMessageParser();
|
{
|
||||||
|
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);
|
var parseResult = parser.ParseMessage(result.Buffer, out consumed, out examined, out message);
|
||||||
Assert.Equal(ServerSentEventsMessageParser.ParseResult.Incomplete, parseResult);
|
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
|
Assert.Equal(expectedResult, parseResult);
|
||||||
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(expectedMessageType, message.Type);
|
Assert.Equal(expectedMessageType, message.Type);
|
||||||
Assert.Equal(consumed, examined);
|
Assert.Equal(consumed, examined);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,9 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("Hello World", "data: T\r\ndata: Hello World\r\n\r\n")]
|
[InlineData("Hello World", ":\r\ndata: 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\nWorld", ":\r\ndata: 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\r\nWorld", ":\r\ndata: T\r\ndata: Hello\r\ndata: World\r\n\r\n")]
|
||||||
public async Task SSEAddsAppropriateFraming(string message, string expected)
|
public async Task SSEAddsAppropriateFraming(string message, string expected)
|
||||||
{
|
{
|
||||||
var channel = Channel.CreateUnbounded<Message>();
|
var channel = Channel.CreateUnbounded<Message>();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue