Merge remote-tracking branch 'origin/release/2.1' into dev
This commit is contained in:
commit
678265259d
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -544,6 +544,33 @@ describe("hubConnection", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("transport falls back from WebSockets to SSE or LongPolling", async (done) => {
|
||||
// Replace Websockets with a function that just
|
||||
// throws to force fallback.
|
||||
const oldWebSocket = (window as any).WebSocket;
|
||||
(window as any).WebSocket = () => {
|
||||
throw new Error("Kick rocks");
|
||||
};
|
||||
|
||||
const hubConnection = new HubConnection(TESTHUBENDPOINT_URL, {
|
||||
logger: LogLevel.Trace,
|
||||
protocol: new JsonHubProtocol(),
|
||||
});
|
||||
|
||||
try {
|
||||
await hubConnection.start();
|
||||
|
||||
// Make sure that we connect with SSE or LongPolling after Websockets fail
|
||||
const transportName = await hubConnection.invoke("GetActiveTransportName");
|
||||
expect(transportName === "ServerSentEvents" || transportName === "LongPolling").toBe(true);
|
||||
} catch (e) {
|
||||
fail(e);
|
||||
} finally {
|
||||
(window as any).WebSocket = oldWebSocket;
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
function getJwtToken(url): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
|
|
|||
|
|
@ -134,6 +134,24 @@ describe("HttpConnection", () => {
|
|||
done();
|
||||
});
|
||||
|
||||
it("start throws after all transports fail", async (done) => {
|
||||
const options: IHttpConnectionOptions = {
|
||||
httpClient: new TestHttpClient()
|
||||
.on("POST", (r) => ({ connectionId: "42", availableTransports: [] }))
|
||||
.on("GET", (r) => { throw new Error("fail"); }),
|
||||
} as IHttpConnectionOptions;
|
||||
|
||||
const connection = new HttpConnection("http://tempuri.org?q=myData", options);
|
||||
try {
|
||||
await connection.start(TransferFormat.Text);
|
||||
fail();
|
||||
done();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("Unable to initialize any of the available transports.");
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
it("preserves user's query string", async (done) => {
|
||||
let connectUrl: string;
|
||||
const fakeTransport: ITransport = {
|
||||
|
|
|
|||
|
|
@ -79,39 +79,29 @@ export class HttpConnection implements IConnection {
|
|||
// No need to add a connection ID in this case
|
||||
this.url = this.baseUrl;
|
||||
this.transport = this.constructTransport(TransportType.WebSockets);
|
||||
// We should just call connect directly in this case.
|
||||
// No fallback or negotiate in this case.
|
||||
await this.transport.connect(this.url, transferFormat, this);
|
||||
} else {
|
||||
let headers;
|
||||
const token = this.options.accessTokenFactory();
|
||||
let headers;
|
||||
if (token) {
|
||||
headers = {
|
||||
["Authorization"]: `Bearer ${token}`,
|
||||
};
|
||||
}
|
||||
|
||||
const negotiatePayload = await this.httpClient.post(this.resolveNegotiateUrl(this.baseUrl), {
|
||||
content: "",
|
||||
headers,
|
||||
});
|
||||
|
||||
const negotiateResponse: INegotiateResponse = JSON.parse(negotiatePayload.content as string);
|
||||
this.connectionId = negotiateResponse.connectionId;
|
||||
|
||||
const negotiateResponse = await this.getNegotiationResponse(headers);
|
||||
// the user tries to stop the the connection when it is being started
|
||||
if (this.connectionState === ConnectionState.Disconnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.connectionId) {
|
||||
this.url = this.baseUrl + (this.baseUrl.indexOf("?") === -1 ? "?" : "&") + `id=${this.connectionId}`;
|
||||
this.transport = this.createTransport(this.options.transport, negotiateResponse.availableTransports, transferFormat);
|
||||
}
|
||||
await this.createTransport(this.options.transport, negotiateResponse, transferFormat, headers);
|
||||
}
|
||||
|
||||
this.transport.onreceive = this.onreceive;
|
||||
this.transport.onclose = (e) => this.stopConnection(true, e);
|
||||
|
||||
await this.transport.connect(this.url, transferFormat, this);
|
||||
|
||||
// only change the state if we were connecting to not overwrite
|
||||
// the state if the connection is already marked as Disconnected
|
||||
this.changeState(ConnectionState.Connecting, ConnectionState.Connected);
|
||||
|
|
@ -123,16 +113,51 @@ export class HttpConnection implements IConnection {
|
|||
}
|
||||
}
|
||||
|
||||
private createTransport(requestedTransport: TransportType | ITransport, availableTransports: IAvailableTransport[], requestedTransferFormat: TransferFormat): ITransport {
|
||||
private async getNegotiationResponse(headers: any): Promise<INegotiateResponse> {
|
||||
const response = await this.httpClient.post(this.resolveNegotiateUrl(this.baseUrl), {
|
||||
content: "",
|
||||
headers,
|
||||
});
|
||||
return JSON.parse(response.content as string);
|
||||
}
|
||||
|
||||
private updateConnectionId(negotiateResponse: INegotiateResponse) {
|
||||
this.connectionId = negotiateResponse.connectionId;
|
||||
this.url = this.baseUrl + (this.baseUrl.indexOf("?") === -1 ? "?" : "&") + `id=${this.connectionId}`;
|
||||
}
|
||||
|
||||
private async createTransport(requestedTransport: TransportType | ITransport, negotiateResponse: INegotiateResponse, requestedTransferFormat: TransferFormat, headers: any): Promise<void> {
|
||||
this.updateConnectionId(negotiateResponse);
|
||||
if (this.isITransport(requestedTransport)) {
|
||||
this.logger.log(LogLevel.Trace, "Connection was provided an instance of ITransport, using that directly.");
|
||||
return requestedTransport;
|
||||
this.transport = requestedTransport;
|
||||
await this.transport.connect(this.url, requestedTransferFormat, this);
|
||||
|
||||
// only change the state if we were connecting to not overwrite
|
||||
// the state if the connection is already marked as Disconnected
|
||||
this.changeState(ConnectionState.Connecting, ConnectionState.Connected);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const endpoint of availableTransports) {
|
||||
const transports = negotiateResponse.availableTransports;
|
||||
for (const endpoint of transports) {
|
||||
this.connectionState = ConnectionState.Connecting;
|
||||
const transport = this.resolveTransport(endpoint, requestedTransport, requestedTransferFormat);
|
||||
if (typeof transport === "number") {
|
||||
return this.constructTransport(transport);
|
||||
this.transport = this.constructTransport(transport);
|
||||
if (negotiateResponse.connectionId === null) {
|
||||
negotiateResponse = await this.getNegotiationResponse(headers);
|
||||
this.updateConnectionId(negotiateResponse);
|
||||
}
|
||||
try {
|
||||
await this.transport.connect(this.url, requestedTransferFormat, this);
|
||||
this.changeState(ConnectionState.Connecting, ConnectionState.Connected);
|
||||
return;
|
||||
} catch (ex) {
|
||||
this.logger.log(LogLevel.Error, `Failed to start the transport' ${TransportType[transport]}:' transport'${ex}'`);
|
||||
this.connectionState = ConnectionState.Disconnected;
|
||||
negotiateResponse.connectionId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
|
||||
private static JsonTextReader CreateJsonTextReader(ReadOnlyMemory<byte> payload)
|
||||
{
|
||||
var textReader = new Utf8BufferTextReader(payload);
|
||||
var textReader = new Utf8BufferTextReader();
|
||||
textReader.SetBuffer(payload);
|
||||
var reader = new JsonTextReader(textReader);
|
||||
reader.ArrayPool = JsonArrayPool<char>.Shared;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
{
|
||||
}
|
||||
|
||||
// Initialize with capacity 2 for the 2 built in protocols
|
||||
private object _lock = new object();
|
||||
private readonly List<SerializedMessage> _serializedMessages = new List<SerializedMessage>(2);
|
||||
private List<SerializedMessage> _serializedMessages;
|
||||
|
||||
public byte[] WriteMessage(IHubProtocol protocol)
|
||||
{
|
||||
|
|
@ -25,7 +24,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
|
||||
lock (_lock)
|
||||
{
|
||||
for (var i = 0; i < _serializedMessages.Count; i++)
|
||||
for (var i = 0; i < _serializedMessages?.Count; i++)
|
||||
{
|
||||
if (_serializedMessages[i].Protocol.Equals(protocol))
|
||||
{
|
||||
|
|
@ -35,6 +34,12 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
|
||||
var bytes = protocol.WriteToArray(this);
|
||||
|
||||
if (_serializedMessages == null)
|
||||
{
|
||||
// Initialize with capacity 2 for the 2 built in protocols
|
||||
_serializedMessages = new List<SerializedMessage>(2);
|
||||
}
|
||||
|
||||
// We don't want to balloon memory if someone writes a poor IHubProtocolResolver
|
||||
// So we cap how many caches we store and worst case just serialize the message for every connection
|
||||
if (_serializedMessages.Count < 10)
|
||||
|
|
|
|||
|
|
@ -58,11 +58,19 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
{
|
||||
while (TextMessageParser.TryParseMessage(ref input, out var payload))
|
||||
{
|
||||
var textReader = new Utf8BufferTextReader(payload);
|
||||
var message = ParseMessage(textReader, binder);
|
||||
if (message != null)
|
||||
var textReader = Utf8BufferTextReader.Get(payload);
|
||||
|
||||
try
|
||||
{
|
||||
messages.Add(message);
|
||||
var message = ParseMessage(textReader, binder);
|
||||
if (message != null)
|
||||
{
|
||||
messages.Add(message);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Utf8BufferTextReader.Return(textReader);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +111,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
using (var reader = new JsonTextReader(textReader))
|
||||
{
|
||||
reader.ArrayPool = JsonArrayPool<char>.Shared;
|
||||
reader.CloseInput = false;
|
||||
|
||||
JsonUtils.CheckRead(reader);
|
||||
|
||||
|
|
@ -559,7 +568,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
|
||||
private object[] BindArguments(JsonTextReader reader, IReadOnlyList<Type> paramTypes)
|
||||
{
|
||||
var arguments = new object[paramTypes.Count];
|
||||
object[] arguments = null;
|
||||
var paramIndex = 0;
|
||||
var argumentsCount = 0;
|
||||
|
||||
|
|
@ -572,7 +581,12 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
throw new InvalidDataException($"Invocation provides {argumentsCount} argument(s) but target expects {paramTypes.Count}.");
|
||||
}
|
||||
|
||||
return arguments;
|
||||
return arguments ?? Array.Empty<object>();
|
||||
}
|
||||
|
||||
if (arguments == null)
|
||||
{
|
||||
arguments = new object[paramTypes.Count];
|
||||
}
|
||||
|
||||
try
|
||||
|
|
@ -608,12 +622,18 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
|
||||
private object[] BindArguments(JArray args, IReadOnlyList<Type> paramTypes)
|
||||
{
|
||||
var arguments = new object[args.Count];
|
||||
if (paramTypes.Count != arguments.Length)
|
||||
if (paramTypes.Count != args.Count)
|
||||
{
|
||||
throw new InvalidDataException($"Invocation provides {arguments.Length} argument(s) but target expects {paramTypes.Count}.");
|
||||
throw new InvalidDataException($"Invocation provides {args.Count} argument(s) but target expects {paramTypes.Count}.");
|
||||
}
|
||||
|
||||
if (paramTypes.Count == 0)
|
||||
{
|
||||
return Array.Empty<object>();
|
||||
}
|
||||
|
||||
var arguments = new object[args.Count];
|
||||
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < paramTypes.Count; i++)
|
||||
|
|
|
|||
|
|
@ -11,10 +11,54 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
internal class Utf8BufferTextReader : TextReader
|
||||
{
|
||||
private ReadOnlyMemory<byte> _utf8Buffer;
|
||||
private Decoder _decoder;
|
||||
|
||||
public Utf8BufferTextReader(ReadOnlyMemory<byte> utf8Buffer)
|
||||
[ThreadStatic]
|
||||
private static Utf8BufferTextReader _cachedInstance;
|
||||
|
||||
#if DEBUG
|
||||
private bool _inUse;
|
||||
#endif
|
||||
|
||||
public Utf8BufferTextReader()
|
||||
{
|
||||
_decoder = Encoding.UTF8.GetDecoder();
|
||||
}
|
||||
|
||||
public static Utf8BufferTextReader Get(ReadOnlyMemory<byte> utf8Buffer)
|
||||
{
|
||||
var reader = _cachedInstance;
|
||||
if (reader == null)
|
||||
{
|
||||
reader = new Utf8BufferTextReader();
|
||||
}
|
||||
|
||||
// Taken off the the thread static
|
||||
_cachedInstance = null;
|
||||
#if DEBUG
|
||||
if (reader._inUse)
|
||||
{
|
||||
throw new InvalidOperationException("The reader wasn't returned!");
|
||||
}
|
||||
|
||||
reader._inUse = true;
|
||||
#endif
|
||||
reader.SetBuffer(utf8Buffer);
|
||||
return reader;
|
||||
}
|
||||
|
||||
public static void Return(Utf8BufferTextReader reader)
|
||||
{
|
||||
_cachedInstance = reader;
|
||||
#if DEBUG
|
||||
reader._inUse = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
public void SetBuffer(ReadOnlyMemory<byte> utf8Buffer)
|
||||
{
|
||||
_utf8Buffer = utf8Buffer;
|
||||
_decoder.Reset();
|
||||
}
|
||||
|
||||
public override int Read(char[] buffer, int index, int count)
|
||||
|
|
@ -25,33 +69,24 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
}
|
||||
|
||||
var source = _utf8Buffer.Span;
|
||||
var destination = new Span<char>(buffer, index, count);
|
||||
var destinationBytesCount = Encoding.UTF8.GetByteCount(buffer, index, count);
|
||||
|
||||
// We have then the destination
|
||||
if (source.Length > destinationBytesCount)
|
||||
{
|
||||
source = source.Slice(0, destinationBytesCount);
|
||||
|
||||
_utf8Buffer = _utf8Buffer.Slice(destinationBytesCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
_utf8Buffer = ReadOnlyMemory<byte>.Empty;
|
||||
}
|
||||
|
||||
var bytesUsed = 0;
|
||||
var charsUsed = 0;
|
||||
#if NETCOREAPP2_1
|
||||
return Encoding.UTF8.GetChars(source, destination);
|
||||
var destination = new Span<char>(buffer, index, count);
|
||||
_decoder.Convert(source, destination, false, out bytesUsed, out charsUsed, out var completed);
|
||||
#else
|
||||
unsafe
|
||||
{
|
||||
fixed (char* destinationChars = &MemoryMarshal.GetReference(destination))
|
||||
fixed (char* destinationChars = &buffer[index])
|
||||
fixed (byte* sourceBytes = &MemoryMarshal.GetReference(source))
|
||||
{
|
||||
return Encoding.UTF8.GetChars(sourceBytes, source.Length, destinationChars, destination.Length);
|
||||
_decoder.Convert(sourceBytes, source.Length, destinationChars, count, false, out bytesUsed, out charsUsed, out var completed);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
_utf8Buffer = _utf8Buffer.Slice(bytesUsed);
|
||||
|
||||
return charsUsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@
|
|||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.SignalR.Tests.Utils, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.SignalR.Tests.Utils, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.SignalR.Common.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
@ -14,6 +14,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http.Internal;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Internal;
|
||||
using Microsoft.AspNetCore.Sockets.Http.Internal;
|
||||
using Microsoft.AspNetCore.Sockets.Internal;
|
||||
|
|
@ -26,7 +27,9 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
public partial class HttpConnection : IConnection
|
||||
{
|
||||
private static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(120);
|
||||
#if !NETCOREAPP2_1
|
||||
private static readonly Version Windows8Version = new Version(6, 2);
|
||||
#endif
|
||||
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ILogger _logger;
|
||||
|
|
@ -99,10 +102,11 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
|
||||
private HttpClient CreateHttpClient()
|
||||
{
|
||||
HttpMessageHandler httpMessageHandler = null;
|
||||
var httpClientHandler = new HttpClientHandler();
|
||||
HttpMessageHandler httpMessageHandler = httpClientHandler;
|
||||
|
||||
if (_httpOptions != null)
|
||||
{
|
||||
var httpClientHandler = new HttpClientHandler();
|
||||
if (_httpOptions.Proxy != null)
|
||||
{
|
||||
httpClientHandler.Proxy = _httpOptions.Proxy;
|
||||
|
|
@ -135,7 +139,10 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
}
|
||||
}
|
||||
|
||||
var httpClient = httpMessageHandler == null ? new HttpClient() : new HttpClient(httpMessageHandler);
|
||||
// Wrap message handler in a logging handler last to ensure it is always present
|
||||
httpMessageHandler = new LoggingHttpMessageHandler(httpMessageHandler, _loggerFactory);
|
||||
|
||||
var httpClient = new HttpClient(httpMessageHandler);
|
||||
httpClient.Timeout = HttpClientTimeout;
|
||||
|
||||
return httpClient;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
// 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.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client.Http.Internal
|
||||
{
|
||||
public class LoggingHttpMessageHandler : DelegatingHandler
|
||||
{
|
||||
private readonly ILogger<LoggingHttpMessageHandler> _logger;
|
||||
|
||||
public LoggingHttpMessageHandler(HttpMessageHandler inner, ILoggerFactory loggerFactory) : base(inner)
|
||||
{
|
||||
if (loggerFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(loggerFactory));
|
||||
}
|
||||
|
||||
_logger = loggerFactory.CreateLogger<LoggingHttpMessageHandler>();
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
Log.SendingHttpRequest(_logger, request.RequestUri);
|
||||
|
||||
var response = await base.SendAsync(request, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Log.UnsuccessfulHttpResponse(_logger, request.RequestUri, response.StatusCode);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, Uri, Exception> _sendingHttpRequest =
|
||||
LoggerMessage.Define<Uri>(LogLevel.Trace, new EventId(1, "SendingHttpRequest"), "Sending HTTP request to '{RequestUrl}'.");
|
||||
|
||||
private static readonly Action<ILogger, Uri, HttpStatusCode, Exception> _unsuccessfulHttpResponse =
|
||||
LoggerMessage.Define<Uri, HttpStatusCode>(LogLevel.Warning, new EventId(2, "UnsuccessfulHttpResponse"), "Unsuccessful HTTP response status code of {StatusCode} return from '{RequestUrl}'.");
|
||||
|
||||
public static void SendingHttpRequest(ILogger logger, Uri requestUrl)
|
||||
{
|
||||
_sendingHttpRequest(logger, requestUrl, null);
|
||||
}
|
||||
public static void UnsuccessfulHttpResponse(ILogger logger, Uri requestUrl, HttpStatusCode statusCode)
|
||||
{
|
||||
_unsuccessfulHttpResponse(logger, requestUrl, statusCode, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,8 +12,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, TransferFormat, Exception> _startTransport =
|
||||
LoggerMessage.Define<TransferFormat>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {TransferFormat}.");
|
||||
private static readonly Action<ILogger, TransferFormat, Uri, Exception> _startTransport =
|
||||
LoggerMessage.Define<TransferFormat, Uri>(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {TransferFormat}. Url: '{WebSocketUrl}'.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _transportStopped =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(2, "TransportStopped"), "Transport stopped.");
|
||||
|
|
@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
LoggerMessage.Define<int>(LogLevel.Debug, new EventId(10, "MessageToApp"), "Passing message to application. Payload size: {Count}.");
|
||||
|
||||
private static readonly Action<ILogger, WebSocketCloseStatus?, Exception> _webSocketClosed =
|
||||
LoggerMessage.Define<WebSocketCloseStatus?>(LogLevel.Information, new EventId(11, "WebSocketClosed"), "Websocket closed by the server. Close status {CloseStatus}.");
|
||||
LoggerMessage.Define<WebSocketCloseStatus?>(LogLevel.Information, new EventId(11, "WebSocketClosed"), "WebSocket closed by the server. Close status {CloseStatus}.");
|
||||
|
||||
private static readonly Action<ILogger, WebSocketMessageType, int, bool, Exception> _messageReceived =
|
||||
LoggerMessage.Define<WebSocketMessageType, int, bool>(LogLevel.Debug, new EventId(12, "MessageReceived"), "Message received. Type: {MessageType}, size: {Count}, EndOfMessage: {EndOfMessage}.");
|
||||
|
|
@ -66,9 +66,9 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
private static readonly Action<ILogger, Exception> _cancelMessage =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(18, "CancelMessage"), "Canceled passing message to application.");
|
||||
|
||||
public static void StartTransport(ILogger logger, TransferFormat transferFormat)
|
||||
public static void StartTransport(ILogger logger, TransferFormat transferFormat, Uri webSocketUrl)
|
||||
{
|
||||
_startTransport(logger, transferFormat, null);
|
||||
_startTransport(logger, transferFormat, webSocketUrl, null);
|
||||
}
|
||||
|
||||
public static void TransportStopped(ILogger logger, Exception exception)
|
||||
|
|
|
|||
|
|
@ -109,10 +109,11 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
? WebSocketMessageType.Binary
|
||||
: WebSocketMessageType.Text;
|
||||
|
||||
var resolvedUrl = ResolveWebSocketsUrl(url);
|
||||
|
||||
Log.StartTransport(_logger, transferFormat);
|
||||
Log.StartTransport(_logger, transferFormat, resolvedUrl);
|
||||
|
||||
await Connect(url);
|
||||
await _webSocket.ConnectAsync(resolvedUrl, CancellationToken.None);
|
||||
|
||||
// TODO: Handle TCP connection errors
|
||||
// https://github.com/SignalR/SignalR/blob/1fba14fa3437e24c204dfaf8a18db3fce8acad3c/src/Microsoft.AspNet.SignalR.Core/Owin/WebSockets/WebSocketHandler.cs#L248-L251
|
||||
|
|
@ -324,7 +325,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
ws.State == WebSocketState.CloseSent);
|
||||
}
|
||||
|
||||
private async Task Connect(Uri url)
|
||||
private static Uri ResolveWebSocketsUrl(Uri url)
|
||||
{
|
||||
var uriBuilder = new UriBuilder(url);
|
||||
if (url.Scheme == "http")
|
||||
|
|
@ -336,7 +337,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
uriBuilder.Scheme = "wss";
|
||||
}
|
||||
|
||||
await _webSocket.ConnectAsync(uriBuilder.Uri, CancellationToken.None);
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
public async Task StopAsync()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ using Microsoft.AspNetCore.Client.Tests;
|
|||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
|
@ -172,5 +174,43 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
Assert.Same(httpOptions.Proxy, httpClientHandler.Proxy);
|
||||
Assert.Same(httpOptions.Credentials, httpClientHandler.Credentials);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpRequestAndErrorResponseLogged()
|
||||
{
|
||||
var testHttpHandler = new TestHttpMessageHandler(false);
|
||||
|
||||
testHttpHandler.OnNegotiate((request, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.BadGateway));
|
||||
|
||||
var httpOptions = new HttpOptions();
|
||||
httpOptions.HttpMessageHandler = inner => testHttpHandler;
|
||||
|
||||
const string loggerName = "Microsoft.AspNetCore.Sockets.Client.Http.Internal.LoggingHttpMessageHandler";
|
||||
var testSink = new TestSink();
|
||||
var logger = new TestLogger(loggerName, testSink, true);
|
||||
|
||||
var mockLoggerFactory = new Mock<ILoggerFactory>();
|
||||
mockLoggerFactory
|
||||
.Setup(m => m.CreateLogger(It.IsAny<string>()))
|
||||
.Returns((string categoryName) => (categoryName == loggerName) ? (ILogger)logger : NullLogger.Instance);
|
||||
|
||||
try
|
||||
{
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(httpOptions, loggerFactory: mockLoggerFactory.Object, url: "http://fakeuri.org/"),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore connection error
|
||||
}
|
||||
|
||||
Assert.Equal(2, testSink.Writes.Count);
|
||||
Assert.Equal("SendingHttpRequest", testSink.Writes[0].EventId.Name);
|
||||
Assert.Equal("UnsuccessfulHttpResponse", testSink.Writes[1].EventId.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
// 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.Buffers;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
||||
{
|
||||
public class Utf8BufferTextReaderTests
|
||||
{
|
||||
[Fact]
|
||||
public void ReadingWhenCharBufferBigEnough()
|
||||
{
|
||||
var buffer = Encoding.UTF8.GetBytes("Hello World");
|
||||
var reader = new Utf8BufferTextReader();
|
||||
reader.SetBuffer(buffer);
|
||||
|
||||
var chars = new char[1024];
|
||||
int read = reader.Read(chars, 0, chars.Length);
|
||||
|
||||
Assert.Equal("Hello World", new string(chars, 0, read));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadingUnicodeWhenCharBufferBigEnough()
|
||||
{
|
||||
var buffer = Encoding.UTF8.GetBytes("a\u00E4\u00E4\u00a9o");
|
||||
var reader = new Utf8BufferTextReader();
|
||||
reader.SetBuffer(buffer);
|
||||
|
||||
var chars = new char[1024];
|
||||
int read = reader.Read(chars, 0, chars.Length);
|
||||
|
||||
Assert.Equal(5, read);
|
||||
Assert.Equal("a\u00E4\u00E4\u00a9o", new string(chars, 0, read));
|
||||
|
||||
read = reader.Read(chars, 0, chars.Length);
|
||||
|
||||
Assert.Equal(0, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadingWhenCharBufferBigEnoughAndNotStartingFromZero()
|
||||
{
|
||||
var buffer = Encoding.UTF8.GetBytes("Hello World");
|
||||
var reader = new Utf8BufferTextReader();
|
||||
reader.SetBuffer(buffer);
|
||||
|
||||
var chars = new char[1024];
|
||||
int read = reader.Read(chars, 10, chars.Length - 10);
|
||||
|
||||
Assert.Equal(11, read);
|
||||
Assert.Equal("Hello World", new string(chars, 10, read));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadingWhenBufferTooSmall()
|
||||
{
|
||||
var buffer = Encoding.UTF8.GetBytes("Hello World");
|
||||
var reader = new Utf8BufferTextReader();
|
||||
reader.SetBuffer(buffer);
|
||||
|
||||
var chars = new char[5];
|
||||
int read = reader.Read(chars, 0, chars.Length);
|
||||
|
||||
Assert.Equal(5, read);
|
||||
Assert.Equal("Hello", new string(chars, 0, read));
|
||||
|
||||
read = reader.Read(chars, 0, chars.Length);
|
||||
|
||||
Assert.Equal(5, read);
|
||||
Assert.Equal(" Worl", new string(chars, 0, read));
|
||||
|
||||
read = reader.Read(chars, 0, chars.Length);
|
||||
|
||||
Assert.Equal(1, read);
|
||||
Assert.Equal("d", new string(chars, 0, read));
|
||||
|
||||
read = reader.Read(chars, 0, chars.Length);
|
||||
|
||||
Assert.Equal(0, read);
|
||||
|
||||
read = reader.Read(chars, 0, 1);
|
||||
|
||||
Assert.Equal(0, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadingUnicodeWhenBufferTooSmall()
|
||||
{
|
||||
var buffer = Encoding.UTF8.GetBytes("\u00E4\u00E4\u00E5");
|
||||
var reader = new Utf8BufferTextReader();
|
||||
reader.SetBuffer(buffer);
|
||||
|
||||
var chars = new char[2];
|
||||
int read = reader.Read(chars, 0, chars.Length);
|
||||
|
||||
Assert.Equal(2, read);
|
||||
Assert.Equal("\u00E4\u00E4", new string(chars, 0, read));
|
||||
|
||||
read = reader.Read(chars, 0, chars.Length);
|
||||
|
||||
Assert.Equal(1, read);
|
||||
Assert.Equal("\u00E5", new string(chars, 0, read));
|
||||
|
||||
read = reader.Read(chars, 0, chars.Length);
|
||||
|
||||
Assert.Equal(0, read);
|
||||
|
||||
read = reader.Read(chars, 0, 1);
|
||||
|
||||
Assert.Equal(0, read);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue