3
votes

I have an ASP.NET Core 2.1 webapi running that is listening for web socket requests. I want to return this.BadRequest() if the request isn't for a WebSocket upgrade.

[Route("api/[controller]")]
[ApiController]
public class SocketController : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> Post()
    {
        if (!this.HttpContext.WebSockets.IsWebSocketRequest)
            return this.BadRequest();

        using (var webSocket = await this.HttpContext.WebSockets.AcceptWebSocketAsync())
        {
            using (var jsonRpc = new JsonRpc(new WebSocketMessageHandler(webSocket)))
            {
                jsonRpc.AddLocalRpcTarget(new SocketServer());
                jsonRpc.StartListening();
                await jsonRpc.Completion;
            }
        }

        return this.Ok();
    }
}

But whether it takes the success path or the failure path, the server throws an exception:

fail: Microsoft.AspNetCore.Server.Kestrel[13] Connection id "0HLEM87126JEO", Request id "0HLEM87126JEO:00000001": An unhandled exception was thrown by the application. System.InvalidOperationException: StatusCode cannot be set because the response has already started. at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ThrowResponseAlreadyStartedException(String value) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.set_StatusCode(Int32 value) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.set_StatusCode(Int32 value) at Microsoft.AspNetCore.Http.Internal.DefaultHttpResponse.set_StatusCode(Int32 value) at Microsoft.AspNetCore.Mvc.StatusCodeResult.ExecuteResult(ActionContext context) at Microsoft.AspNetCore.Mvc.ActionResult.ExecuteResultAsync(ActionContext context) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeResultAsync(IActionResult result) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResultFilterAsyncTFilter,TFilterAsync at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResultExecutedContext context) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeResultFilters() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync() at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

It works OK if I change the method to just return Task so I don't have to provide an IActionResult, but then I don't have a way to return a 400 response to bad requests. What is the best way to go about this?

My middleware is super simple:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            // app.UseHsts();
        }

        // app.UseHttpsRedirection();
        app.UseWebSockets();
        app.UseMvc();
    }
1
The problem is that the websocket handler will likely already send something for the communication to take place. I guess that there is a way to use the current HttpContext to provide a response instead of providing one by returning from the function. I don't know anything about asp.net though.Dennis Kuypers
Most likely a middleware higher up in the pipeline has already started responding because of the socket request. Should start investigating thereNkosi
Thanks, @nkosi. My middleware is super simple. I've added it to my question.Andrew Arnott

1 Answers

4
votes

The secret was EmptyResult, for which there is no this.Empty() method.

public async Task<IActionResult> Post()
{
    if (!this.HttpContext.WebSockets.IsWebSocketRequest)
    {
        return this.BadRequest();
    }

    using (var webSocket = await this.HttpContext.WebSockets.AcceptWebSocketAsync())
    {
        using (var jsonRpc = new JsonRpc(new WebSocketMessageHandler(webSocket)))
        {
            jsonRpc.AddLocalRpcTarget(new SocketServer());
            jsonRpc.StartListening();
            await jsonRpc.Completion;
        }
    }

    return new EmptyResult();
}