commit
7e524a7b58
|
|
@ -22,7 +22,7 @@
|
|||
</Target>
|
||||
|
||||
<PropertyGroup>
|
||||
<TestDependsOn>$(TestDependsOn);RunTSClientNodeTests;RunBrowserTests</TestDependsOn>
|
||||
<TestDependsOn>$(TestDependsOn);RunTSClientNodeTests;RunBrowserTests;RunJavaTests</TestDependsOn>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="RunTSClientNodeTests">
|
||||
|
|
@ -35,9 +35,14 @@
|
|||
<Exec Command="npm run test:inner -- --no-color --configuration $(Configuration)" WorkingDirectory="$(RepositoryRoot)clients/ts/FunctionalTests" IgnoreStandardErrorWarningFormat="true" />
|
||||
</Target>
|
||||
|
||||
<Target Name="RunJavaTests" Condition="'$(JAVA_HOME)' != '' And Exists('$(JAVA_HOME)/lib/tools.jar')">
|
||||
<Message Text="Running Java client tests" Importance="high" />
|
||||
<Exec Command="./gradlew test" WorkingDirectory="$(RepositoryRoot)clients/java/signalr" IgnoreStandardErrorWarningFormat="true" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup>
|
||||
<GetArtifactInfoDependsOn>$(GetArtifactInfoDependsOn);GetNpmArtifactInfo</GetArtifactInfoDependsOn>
|
||||
<PrepareDependsOn>$(PrepareDependsOn);GetNpmArtifactInfo</PrepareDependsOn>
|
||||
<GetArtifactInfoDependsOn>$(GetArtifactInfoDependsOn);GetNpmArtifactInfo;GetJavaArtifactInfo</GetArtifactInfoDependsOn>
|
||||
<PrepareDependsOn>$(PrepareDependsOn);GetNpmArtifactInfo;GetJavaArtifactInfo</PrepareDependsOn>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="GetNpmArtifactInfo">
|
||||
|
|
@ -57,12 +62,32 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FilesToExcludeFromSigning Include="%(NPMPackage.ArtifactPath)" />
|
||||
<FilesToExcludeFromSigning Include="%(NPMPackage.ArtifactPath);" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<Target Name="GetJavaArtifactInfo" Condition="'$(JAVA_HOME)' != '' And Exists('$(JAVA_HOME)/lib/tools.jar')">
|
||||
<ItemGroup>
|
||||
<ArtifactInfo Include="$(BuildDir)\%(Jars.Identity)">
|
||||
<ArtifactType>JavaJar</ArtifactType>
|
||||
<Version>$(JavaClientVersion)</Version>
|
||||
<Category>ship</Category>
|
||||
</ArtifactInfo>
|
||||
<ArtifactInfo Include="$(BuildDir)\%(PomFile.Identity)">
|
||||
<ArtifactType>MavenPOM</ArtifactType>
|
||||
<Version>$(JavaClientVersion)</Version>
|
||||
<Category>ship</Category>
|
||||
</ArtifactInfo>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FilesToExcludeFromSigning Include="$(BuildDir)\%(Jars.Identity)" />
|
||||
<FilesToExcludeFromSigning Include="$(BuildDir)\%(PomFile.Identity)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<PropertyGroup>
|
||||
<CompileDependsOn>Restore;BuildNPMPackages;$(CompileDependsOn)</CompileDependsOn>
|
||||
<CompileDependsOn>Restore;BuildNPMPackages;$(CompileDependsOn);BuildJavaClient</CompileDependsOn>
|
||||
</PropertyGroup>
|
||||
<Target Name="BuildNPMPackages" DependsOnTargets="RestoreNpm;GetNpmArtifactInfo">
|
||||
<Message Text="Building %(NPMPackage.PackageId)..." Importance="high" />
|
||||
|
|
@ -71,8 +96,13 @@
|
|||
<Exec Command="npm run build" WorkingDirectory="$(RepositoryRoot)clients/ts/FunctionalTests" IgnoreStandardErrorWarningFormat="true" />
|
||||
</Target>
|
||||
|
||||
<Target Name="BuildJavaClient" Condition="'$(JAVA_HOME)' != '' And Exists('$(JAVA_HOME)/lib/tools.jar')" DependsOnTargets="GetJavaArtifactInfo">
|
||||
<Message Text="Building Java client" Importance="high" />
|
||||
<Exec Command="./gradlew compileJava" WorkingDirectory="$(RepositoryRoot)clients/java/signalr" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageDependsOn>Compile;PackNPMPackages;$(PackageDependsOn)</PackageDependsOn>
|
||||
<PackageDependsOn>Compile;PackNPMPackages;$(PackageDependsOn);PackJavaClient</PackageDependsOn>
|
||||
</PropertyGroup>
|
||||
<Target Name="PackNPMPackages" DependsOnTargets="BuildNPMPackages">
|
||||
<Message Text="Packing %(NPMPackage.PackageId)..." Importance="high" />
|
||||
|
|
@ -83,4 +113,24 @@
|
|||
<Move SourceFiles="%(NPMPackage.OutputTar)" DestinationFiles="%(NPMPackage.ArtifactPath)" />
|
||||
<Move SourceFiles="%(NPMPackage.PackageJson).bak" DestinationFiles="%(NPMPackage.PackageJson)" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<JavaBuildFiles Include="signalr-client-$(JavaClientVersion).jar;signalr-client-$(JavaClientVersion)-javadoc.jar;signalr-client-$(JavaClientVersion)-sources.jar;signalr-client-$(JavaClientVersion).pom"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Jars Include="signalr-client-$(JavaClientVersion).jar;signalr-client-$(JavaClientVersion)-javadoc.jar;signalr-client-$(JavaClientVersion)-sources.jar;" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PomFile Include="signalr-client-$(JavaClientVersion).pom" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PackJavaClient" Condition="'$(JAVA_HOME)' != '' And Exists('$(JAVA_HOME)/lib/tools.jar')">
|
||||
<Message Text="Packing Java client" Importance="high" />
|
||||
<Exec Command="./gradlew jar sourceJar javadocJar generatePOM" WorkingDirectory="$(RepositoryRoot)clients/java/signalr" />
|
||||
<Move SourceFiles="$(RepositoryRoot)clients/java/signalr/signalr-client-$(JavaClientVersion).pom" DestinationFolder="$(RepositoryRoot)clients/java/signalr\build\libs" />
|
||||
<Copy SourceFiles="$(RepositoryRoot)clients/java/signalr\build\libs\%(JavaBuildFiles.Identity)" DestinationFolder="$(BuildDir)" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
rootProject.name = 'client'
|
||||
rootProject.name = 'signalr-client'
|
||||
include 'main'
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TestConnectionHandler>();
|
||||
var builder = new ConnectionBuilder(services.BuildServiceProvider());
|
||||
builder.UseConnectionHandler<TestConnectionHandler>();
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<JavaClientVersion>0.1.0-preview1</JavaClientVersion>
|
||||
<VersionPrefix>3.0.0</VersionPrefix>
|
||||
<VersionSuffix>alpha1</VersionSuffix>
|
||||
<PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' == 'rtm' ">$(VersionPrefix)</PackageVersion>
|
||||
|
|
|
|||
Loading…
Reference in New Issue