diff --git a/build/repo.targets b/build/repo.targets index f5f6e2871f..1ceaf50df2 100644 --- a/build/repo.targets +++ b/build/repo.targets @@ -22,7 +22,7 @@ - $(TestDependsOn);RunTSClientNodeTests;RunBrowserTests + $(TestDependsOn);RunTSClientNodeTests;RunBrowserTests;RunJavaTests @@ -35,9 +35,14 @@ + + + + + - $(GetArtifactInfoDependsOn);GetNpmArtifactInfo - $(PrepareDependsOn);GetNpmArtifactInfo + $(GetArtifactInfoDependsOn);GetNpmArtifactInfo;GetJavaArtifactInfo + $(PrepareDependsOn);GetNpmArtifactInfo;GetJavaArtifactInfo @@ -57,12 +62,32 @@ - + + + + + + + + JavaJar + $(JavaClientVersion) + ship + + + MavenPOM + $(JavaClientVersion) + ship + + + + + + - Restore;BuildNPMPackages;$(CompileDependsOn) + Restore;BuildNPMPackages;$(CompileDependsOn);BuildJavaClient @@ -71,8 +96,13 @@ + + + + + - Compile;PackNPMPackages;$(PackageDependsOn) + Compile;PackNPMPackages;$(PackageDependsOn);PackJavaClient @@ -83,4 +113,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/clients/java/signalr/build.gradle b/clients/java/signalr/build.gradle index 4033dad394..fe04561d69 100644 --- a/clients/java/signalr/build.gradle +++ b/clients/java/signalr/build.gradle @@ -1,9 +1,10 @@ plugins { id 'java' + id 'maven' } group 'com.microsoft.aspnetcore' -version '0.1.0' +version '0.1.0-preview1' sourceCompatibility = 1.8 @@ -16,3 +17,32 @@ dependencies { implementation "org.java-websocket:Java-WebSocket:1.3.8" implementation 'com.google.code.gson:gson:2.8.5' } + +task sourceJar(type: Jar) { + classifier "sources" + from sourceSets.main.allJava +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier "javadoc" + from javadoc.destinationDir +} + +task generatePOM { + pom { + project { + groupId 'com.microsoft.aspnetcore' + artifactId 'signalr' + version '0.1.0-preview1' + + inceptionYear '2018' + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + } + }.writeTo("signalr-client-0.1.0-preview1.pom") +} \ No newline at end of file diff --git a/clients/java/signalr/gradlew b/clients/java/signalr/gradlew old mode 100644 new mode 100755 diff --git a/clients/java/signalr/settings.gradle b/clients/java/signalr/settings.gradle index 8e0fe12928..90473932b7 100644 --- a/clients/java/signalr/settings.gradle +++ b/clients/java/signalr/settings.gradle @@ -1,3 +1,3 @@ -rootProject.name = 'client' +rootProject.name = 'signalr-client' include 'main' diff --git a/specs/TransportProtocols.md b/specs/TransportProtocols.md index 3d90ceb6cd..4a932037f1 100644 --- a/specs/TransportProtocols.md +++ b/specs/TransportProtocols.md @@ -20,11 +20,11 @@ Throughout this document, the term `[endpoint-base]` is used to refer to the rou The `POST [endpoint-base]/negotiate` request is used to establish a connection between the client and the server. The content type of the response is `application/json`. The response to the `POST [endpoint-base]/negotiate` request contains one of two types of responses: -1. A response that contains the `connectionId` which will be used to identify the connection on the server and the list of the transports supported by the server. +1. A response that contains the `id` which will be used to identify the connection on the server and the list of the transports supported by the server. ``` { - "connectionId":"807809a5-31bf-470d-9e23-afaee35d8a0d", + "id":"807809a5-31bf-470d-9e23-afaee35d8a0d", "availableTransports":[ { "transport": "WebSockets", @@ -44,7 +44,7 @@ The `POST [endpoint-base]/negotiate` request is used to establish a connection b The payload returned from this endpoint provides the following data: - * The `connectionId` which is **required** by the Long Polling and Server-Sent Events transports (in order to correlate sends and receives). + * The `id` which is **required** by the Long Polling and Server-Sent Events transports (in order to correlate sends and receives). * The `availableTransports` list which describes the transports the server supports. For each transport, the name of the transport (`transport`) is listed, as is a list of "transfer formats" supported by the transport (`transferFormats`) @@ -72,7 +72,7 @@ Some transports are limited to supporting only `Text` data (specifically, Server The WebSockets transport is unique in that it is full duplex, and a persistent connection that can be established in a single operation. As a result, the client is not required to use the `POST [endpoint-base]/negotiate` request to establish a connection in advance. It also includes all the necessary metadata in it's own frame metadata. -The WebSocket transport is activated by making a WebSocket connection to `[endpoint-base]`. The **optional** `connectionId` query string value is used to identify the connection to attach to. If there is no `connectionId` query string value, a new connection is established. If the parameter is specified but there is no connection with the specified ID value, a `404 Not Found` response is returned. Upon receiving this request, the connection is established and the server responds with a WebSocket upgrade (`101 Switching Protocols`) immediately ready for frames to be sent/received. The WebSocket OpCode field is used to indicate the type of the frame (Text or Binary). +The WebSocket transport is activated by making a WebSocket connection to `[endpoint-base]`. The **optional** `id` query string value is used to identify the connection to attach to. If there is no `id` query string value, a new connection is established. If the parameter is specified but there is no connection with the specified ID value, a `404 Not Found` response is returned. Upon receiving this request, the connection is established and the server responds with a WebSocket upgrade (`101 Switching Protocols`) immediately ready for frames to be sent/received. The WebSocket OpCode field is used to indicate the type of the frame (Text or Binary). Establishing a second WebSocket connection when there is already a WebSocket connection associated with the Endpoints connection is not permitted and will fail with a `409 Conflict` status code. @@ -84,7 +84,7 @@ HTTP Post is a half-transport, it is only able to send messages from the Client This transport requires that a connection be established using the `POST [endpoint-base]/negotiate` request. -The HTTP POST request is made to the URL `[endpoint-base]`. The **mandatory** `connectionId` query string value is used to identify the connection to send to. If there is no `connectionId` query string value, a `400 Bad Request` response is returned. Upon receipt of the **entire** payload, the server will process the payload and responds with `200 OK` if the payload was successfully processed. If a client makes another request to `/` while an existing request is outstanding, the new request is immediately terminated by the server with the `409 Conflict` status code. +The HTTP POST request is made to the URL `[endpoint-base]`. The **mandatory** `id` query string value is used to identify the connection to send to. If there is no `id` query string value, a `400 Bad Request` response is returned. Upon receipt of the **entire** payload, the server will process the payload and responds with `200 OK` if the payload was successfully processed. If a client makes another request to `/` while an existing request is outstanding, the new request is immediately terminated by the server with the `409 Conflict` status code. If a client receives a `409 Conflict` request, the connection remains open. Any other response indicates that the connection has been terminated due to an error. @@ -109,7 +109,7 @@ foo: boz In the first event, the value of `baz` would be `boz\nbiz\nflarg`, due to the concatenation behavior above. Full details can be found in the spec linked above. -In this transport, the client establishes an SSE connection to `[endpoint-base]` with an `Accept` header of `text/event-stream`, and the server responds with an HTTP response with a `Content-Type` of `text/event-stream`. The **mandatory** `connectionId` query string value is used to identify the connection to send to. If there is no `connectionId` query string value, a `400 Bad Request` response is returned, if there is no connection with the specified ID, a `404 Not Found` response is returned. Each SSE event represents a single frame from client to server. The transport uses unnamed events, which means only the `data` field is available. Thus we use the first line of the `data` field for frame metadata. +In this transport, the client establishes an SSE connection to `[endpoint-base]` with an `Accept` header of `text/event-stream`, and the server responds with an HTTP response with a `Content-Type` of `text/event-stream`. The **mandatory** `id` query string value is used to identify the connection to send to. If there is no `id` query string value, a `400 Bad Request` response is returned, if there is no connection with the specified ID, a `404 Not Found` response is returned. Each SSE event represents a single frame from client to server. The transport uses unnamed events, which means only the `data` field is available. Thus we use the first line of the `data` field for frame metadata. The Server-Sent Events transport only supports text data, because it is a text-based protocol. As a result, it is reported by the server as supporting only the `Text` transfer format. If a client wishes to send arbitrary binary data, it should skip the Server-Sent Events transport when selecting an appropriate transport. @@ -123,10 +123,10 @@ Long Polling requires that the client poll the server for new messages. Unlike t A Poll is established by sending an HTTP GET request to `[endpoint-base]` with the following query string parameters -* `connectionId` (Required) - The Connection ID of the destination connection. +* `id` (Required) - The Connection ID of the destination connection. When data is available, the server responds with a body in one of the two formats below (depending upon the value of the `Accept` header). The response may be chunked, as per the chunked encoding part of the HTTP spec. -If the `connectionId` parameter is missing, a `400 Bad Request` response is returned. If there is no connection with the ID specified in `connectionId`, a `404 Not Found` response is returned. +If the `id` parameter is missing, a `400 Bad Request` response is returned. If there is no connection with the ID specified in `id`, a `404 Not Found` response is returned. -When the client has finished with the connection, it can issue a `DELETE` request to `[endpoint-base]` (with the `connectionId` in the querystring) to gracefully terminate the connection. The server will complete the latest poll with `204` to indicate that it has shut down. +When the client has finished with the connection, it can issue a `DELETE` request to `[endpoint-base]` (with the `id` in the querystring) to gracefully terminate the connection. The server will complete the latest poll with `204` to indicate that it has shut down. diff --git a/src/Microsoft.AspNetCore.Http.Connections/Internal/HttpConnectionDispatcher.cs b/src/Microsoft.AspNetCore.Http.Connections/Internal/HttpConnectionDispatcher.cs index 3d36933d3c..3b18c53bfd 100644 --- a/src/Microsoft.AspNetCore.Http.Connections/Internal/HttpConnectionDispatcher.cs +++ b/src/Microsoft.AspNetCore.Http.Connections/Internal/HttpConnectionDispatcher.cs @@ -289,6 +289,13 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal pollAgain = false; } } + else if (resultTask.IsFaulted) + { + // transport task was faulted, we should remove the connection + await _manager.DisposeAndRemoveAsync(connection, closeGracefully: false); + + pollAgain = false; + } else if (context.Response.StatusCode == StatusCodes.Status204NoContent) { // Don't poll if the transport task was canceled diff --git a/src/Microsoft.AspNetCore.Http.Connections/Internal/Transports/LongPollingTransport.cs b/src/Microsoft.AspNetCore.Http.Connections/Internal/Transports/LongPollingTransport.cs index cf5638d74d..f3cd87f484 100644 --- a/src/Microsoft.AspNetCore.Http.Connections/Internal/Transports/LongPollingTransport.cs +++ b/src/Microsoft.AspNetCore.Http.Connections/Internal/Transports/LongPollingTransport.cs @@ -90,6 +90,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal.Transports catch (Exception ex) { Log.LongPollingTerminated(_logger, ex); + context.Response.ContentType = "text/plain"; + context.Response.StatusCode = StatusCodes.Status500InternalServerError; throw; } } diff --git a/test/Microsoft.AspNetCore.Http.Connections.Tests/HttpConnectionDispatcherTests.cs b/test/Microsoft.AspNetCore.Http.Connections.Tests/HttpConnectionDispatcherTests.cs index 0761fe7c2b..7934e98df1 100644 --- a/test/Microsoft.AspNetCore.Http.Connections.Tests/HttpConnectionDispatcherTests.cs +++ b/test/Microsoft.AspNetCore.Http.Connections.Tests/HttpConnectionDispatcherTests.cs @@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Connections.Internal; +using Microsoft.AspNetCore.Http.Connections.Internal.Transports; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.SignalR.Tests; @@ -2016,6 +2017,48 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests } } + [Fact] + public async Task ErrorDuringPollWillCloseConnection() + { + bool ExpectedErrors(WriteContext writeContext) + { + return (writeContext.LoggerName == typeof(LongPollingTransport).FullName && + writeContext.EventId.Name == "LongPollingTerminated") || + (writeContext.LoggerName == typeof(HttpConnectionManager).FullName && + writeContext.EventId.Name == "FailedDispose"); + } + + using (StartVerifiableLog(out var loggerFactory, LogLevel.Debug, expectedErrorsFilter: ExpectedErrors)) + { + var manager = CreateConnectionManager(loggerFactory); + var connection = manager.CreateConnection(); + connection.TransportType = HttpTransportType.LongPolling; + + var dispatcher = new HttpConnectionDispatcher(manager, loggerFactory); + + var services = new ServiceCollection(); + services.AddSingleton(); + var builder = new ConnectionBuilder(services.BuildServiceProvider()); + builder.UseConnectionHandler(); + var app = builder.Build(); + var options = new HttpConnectionDispatcherOptions(); + + var context = MakeRequest("/foo", connection); + + // Initial poll will complete immediately + await dispatcher.ExecuteAsync(context, options, app).OrTimeout(); + + var pollContext = MakeRequest("/foo", connection); + var pollTask = dispatcher.ExecuteAsync(pollContext, options, app); + // fail LongPollingTransport ReadAsync + connection.Transport.Output.Complete(new InvalidOperationException()); + await pollTask.OrTimeout(); + + Assert.Equal(StatusCodes.Status500InternalServerError, pollContext.Response.StatusCode); + Assert.False(manager.TryGetConnection(connection.ConnectionId, out var _)); + } + } + private class RejectHandler : TestAuthenticationHandler { protected override bool ShouldAccept => false; diff --git a/version.props b/version.props index 71a78cddd8..4ef0f74454 100644 --- a/version.props +++ b/version.props @@ -1,5 +1,6 @@  + 0.1.0-preview1 3.0.0 alpha1 $(VersionPrefix)