dotnet / AspNetCore.Docs

Documentation for ASP.NET Core
https://docs.microsoft.com/aspnet/core
Creative Commons Attribution 4.0 International
12.6k stars 25.3k forks source link

Cannot accept WebSocket Requests in MVC Controller: HttpContext.WebSockets.IsWebSocketRequest is ALWAYS false #21701

Closed princefishthrower closed 2 years ago

princefishthrower commented 3 years ago

Describe the bug

I am unable to accept WebSocket connections in a controller endpoint. The endpoint logic is entered when I debug, but the value of HttpContext.WebSockets.IsWebSocketRequest is ALWAYS false.

ws://, wss://, calling from chrome, calling from firefox, it doesn't matter.

I CAN connect if I use the app.Use way of doing things with websockets in my Startup.cs, but I would greatly prefer containing all my websocket logic in controller instead.

To Reproduce

  1. Create a brand new .NET project with dotnet new webapi -n WebSocketsTutorial

  2. add app.UseWebSockets(); to Startup.cs

  3. Create a new controller named WebSocketsController

paste this code into that controller:

using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace WebSocketsTutorial.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WebSocketsController : ControllerBase
    {
        private readonly ILogger<WebSocketsController> _logger;

        public WebSocketsController(ILogger<WebSocketsController> logger)
        {
            _logger = logger;
        }

        [HttpGet("/ws")]
        public async Task Get()
        {
            // THIS LINE IS THE PROBLEM, IT NEVER ENTERS
            if (HttpContext.WebSockets.IsWebSocketRequest)
            {
                using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
                _logger.Log(LogLevel.Information, "WebSocket connection established");
                await Echo(webSocket);
            }
            else
            {
                HttpContext.Response.StatusCode = 400;
            }
        }

        private async Task Echo(WebSocket webSocket)
        {
            var buffer = new byte[1024 * 4];
            var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
            _logger.Log(LogLevel.Information, "Message received from Client");

            while (!result.CloseStatus.HasValue)
            {
                var serverMsg = Encoding.UTF8.GetBytes($"Server: Hello. You said: {Encoding.UTF8.GetString(buffer)}");
                await webSocket.SendAsync(new ArraySegment<byte>(serverMsg, 0, serverMsg.Length), result.MessageType, result.EndOfMessage, CancellationToken.None);
                _logger.Log(LogLevel.Information, "Message sent to Client");

                result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
                _logger.Log(LogLevel.Information, "Message received from Client");

            }
            await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
            _logger.Log(LogLevel.Information, "WebSocket connection closed");
        }
    }
}

Go into a browser (tested so far on firefox and chrome) and try this:

let webSocket = new WebSocket('wss://localhost:5001/ws');

It will fail with the 400 code, as written in the controller handler.

Can anyone reproduce, or have I lost my mind? What am I missing? Source's I've used:

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/websockets?view=aspnetcore-5.0

https://sahansera.dev/understanding-websockets-with-aspnetcore-5/

Further technical details

.NET SDK (reflecting any global.json):
 Version:   5.0.103
 Commit:    72dec52dbd

Runtime Environment:
 OS Name:     Mac OS X
 OS Version:  10.15
 OS Platform: Darwin
 RID:         osx.10.15-x64
 Base Path:   /usr/local/share/dotnet/sdk/5.0.103/

Host (useful for support):
  Version: 5.0.3
  Commit:  c636bbdc8a

.NET SDKs installed:
  2.1.701 [/usr/local/share/dotnet/sdk]
  2.2.207 [/usr/local/share/dotnet/sdk]
  3.0.101 [/usr/local/share/dotnet/sdk]
  3.1.102 [/usr/local/share/dotnet/sdk]
  3.1.401 [/usr/local/share/dotnet/sdk]
  3.1.402 [/usr/local/share/dotnet/sdk]
  3.1.403 [/usr/local/share/dotnet/sdk]
  3.1.404 [/usr/local/share/dotnet/sdk]
  3.1.405 [/usr/local/share/dotnet/sdk]
  3.1.406 [/usr/local/share/dotnet/sdk]
  5.0.100 [/usr/local/share/dotnet/sdk]
  5.0.101 [/usr/local/share/dotnet/sdk]
  5.0.102 [/usr/local/share/dotnet/sdk]
  5.0.103 [/usr/local/share/dotnet/sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.All 2.1.12 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.8 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.12 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.8 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.0.1 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.2 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.7 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.8 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.9 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.10 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.11 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.12 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.0 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.1 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.2 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.3 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.1.12 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.15 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.21 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.22 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.23 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.8 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.0.1 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.2 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.7 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.8 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.9 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.10 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.11 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.12 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.0 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.1 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.2 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.3 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

To install additional .NET runtimes or SDKs:
  https://aka.ms/dotnet-download

Document Details

Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.

Tratcher commented 3 years ago

I CAN connect if I use the app.Use way of doing things with websockets in my Startup.cs, but I would greatly prefer containing all my websocket logic in controller instead.

To Reproduce

  1. Create a brand new .NET project with dotnet new webapi -n WebSocketsTutorial
  2. add app.UseWebSockets(); to Startup.cs

Can you show your Startup.cs for both of these aproaches? One issue with UseWebSockets is that it needs to be placed early in the pipeline.

princefishthrower commented 3 years ago

@Tratcher - Ah, that was it! Working with the boilerplate Startup.cs, I just tried all possible locations of app.UseWebSockets(), it turns out that app.UseEndpoints() was the culprit - as long as app.UseWebSockets() occurs before that call, it works! Now the question becomes, is this intentional or expected behavior due to the internals of .NET or just a straight-up bug? Either way, it should be mentioned somewhere on the documentation page:

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/websockets?view=aspnetcore-5.0

I've got a pull request ready with an info block below the app.UseWebsockets() code snippet that would have saved me from this problem:

https://github.com/dotnet/AspNetCore.Docs/pull/21695

Tratcher commented 3 years ago

This is intentional, UseWebSockets must run before any components that want to use websockets. In this case you're doing WebSockets in an MVC controller which runs in the UseEndpoints step.

I agree the doc could be clearer about this. I'm going to transfer this to the docs repo.

Rick-Anderson commented 3 years ago

@serpent5 this looks fun, let me know if you want it.

serpent5 commented 3 years ago

@Rick-Anderson Sure. Can you add it to Serp? I'll keep track of things in there. Feel free to prioritise them accordingly. I'll speak up if there's something I'd rather not do.

serpent5 commented 2 years ago

Closing as an example of using a controller has been added since this discussion was opened.

mediawolf commented 1 year ago

Web sockets created with _host.GetTestServer().CreateWebSocketClient() work well even if UseWebSockets() is called after UseEndpoints().