[{"data":1,"prerenderedAt":2301},["ShallowReactive",2],{"\u002Fblog\u002Fsignalr-trap-not-just-websockets":3},{"id":4,"title":5,"body":6,"createdAt":2286,"description":2287,"extension":2288,"externalPost":2289,"githubLink":2253,"language":2290,"meta":2291,"navigation":346,"ogImage":2289,"path":2292,"robots":2289,"seo":2293,"stem":2294,"tags":2295,"__hash__":2300,"date":2286},"blog\u002Fblog\u002Fsignalr-trap-not-just-websockets.md","The SignalR Trap, why it's not just WebSockets",{"type":7,"value":8,"toc":2260},"minimark",[9,21,26,31,38,41,52,55,74,77,80,102,106,120,124,223,226,230,233,263,266,270,273,314,400,403,406,410,416,458,469,501,521,524,533,536,539,545,672,678,681,684,688,691,697,704,710,716,720,723,730,736,740,747,840,844,847,901,906,945,970,974,977,985,988,1005,1008,1012,1015,1053,1057,1060,1064,1067,1079,1082,1087,1096,1128,1273,1310,1314,1323,1327,1338,1344,1348,1351,1354,1361,1364,2194,2201,2205,2208,2211,2214,2217,2221,2256],[10,11,12,13,20],"p",{},"If you work in the .NET ecosystem and need to build real-time communication applications, ",[14,15,19],"a",{"href":16,"rel":17},"https:\u002F\u002Fdotnet.microsoft.com\u002Fen-us\u002Fapps\u002Faspnet\u002Fsignalr",[18],"nofollow","SignalR"," is usually the first choice. But I often see developers confusing SignalR with WebSocket, and it's worth understanding why SignalR is not just WebSocket.",[22,23,25],"h2",{"id":24},"websockets-vs-signalr","WebSockets vs. SignalR",[27,28,30],"h3",{"id":29},"what-is-a-websocket","What is a WebSocket?",[10,32,33,37],{},[34,35,36],"strong",{},"WebSocket"," is a communication protocol, a persistent full-duplex connection between client and server over TCP. It is \"just\" transport: you send bytes, you receive bytes. It does not define a message format or any application logic.",[10,39,40],{},"After the HTTP Upgrade:",[42,43,48],"pre",{"className":44,"code":46,"language":47},[45],"language-text","GET \u002Fchat HTTP\u002F1.1\nHost: example.com\nUpgrade: websocket\nConnection: Upgrade\n","text",[49,50,46],"code",{"__ignoreMap":51},"",[10,53,54],{},"The connection stops being HTTP and becomes just a stream of bytes. The WebSocket protocol knows nothing about:",[56,57,58,62,65,68,71],"ul",{},[59,60,61],"li",{},"methods",[59,63,64],{},"events",[59,66,67],{},"JSON",[59,69,70],{},"users",[59,72,73],{},"groups",[10,75,76],{},"All of that is the application's responsibility.",[10,78,79],{},"You can picture the WebSocket as a pipe that carries bytes between client and server, with no knowledge of what those bytes mean.",[42,81,85],{"className":82,"code":83,"language":84,"meta":51,"style":51},"language-mermaid shiki shiki-themes github-dark","flowchart LR\n  Client \u003C== Bytes ==> Server\n","mermaid",[49,86,87,96],{"__ignoreMap":51},[88,89,92],"span",{"class":90,"line":91},"line",1,[88,93,95],{"class":94},"s95oV","flowchart LR\n",[88,97,99],{"class":90,"line":98},2,[88,100,101],{"class":94},"  Client \u003C== Bytes ==> Server\n",[27,103,105],{"id":104},"what-is-signalr","What is SignalR?",[10,107,108,110,111,115,116,119],{},[34,109,19],{}," is an abstraction library on top of several transports (WebSocket, Server-Sent Events, Long Polling). On the server side, it adds concepts like ",[112,113,114],"em",{},"hubs",", remote method invocation, client groups, and user management. On the client side, it provides libraries that handle message serialization and automatic reconnection. And, importantly for this article, it defines its ",[34,117,118],{},"own message protocol"," on top of the transport.",[27,121,123],{"id":122},"quick-comparison","Quick comparison",[125,126,127,139],"table",{},[128,129,130],"thead",{},[131,132,133,137],"tr",{},[134,135,136],"th",{},"WebSockets",[134,138,19],{},[140,141,142,151,159,167,175,183,191,199,207,215],"tbody",{},[131,143,144,148],{},[145,146,147],"td",{},"Protocol",[145,149,150],{},"Framework",[131,152,153,156],{},[145,154,155],{},"RFC 6455",[145,157,158],{},"ASP.NET Core library",[131,160,161,164],{},[145,162,163],{},"Sends bytes or text",[145,165,166],{},"Sends serialized messages (JSON or MessagePack)",[131,168,169,172],{},[145,170,171],{},"You have to build your own protocol",[145,173,174],{},"SignalR protocol already defined",[131,176,177,180],{},[145,178,179],{},"No automatic reconnection",[145,181,182],{},"Reconnection supported",[131,184,185,188],{},[145,186,187],{},"No user management",[145,189,190],{},"User management built in",[131,192,193,196],{},[145,194,195],{},"No groups",[145,197,198],{},"Groups built in",[131,200,201,204],{},[145,202,203],{},"No broadcast",[145,205,206],{},"Broadcast built in",[131,208,209,212],{},[145,210,211],{},"Works with any technology",[145,213,214],{},"Server in ASP.NET Core; clients for .NET, JS\u002FTS and Java",[131,216,217,220],{},[145,218,219],{},"No specific client required",[145,221,222],{},"Needs a client compatible with the SignalR protocol",[10,224,225],{},"All of this makes development with SignalR extremely simple, but it also makes testing quite a bit harder when you don't have those same abstractions available. That problem is exactly what led me to write this article: understanding a few details of the SignalR protocol so you can run tests when you don't have a SignalR client at hand.",[22,227,229],{"id":228},"what-signalr-adds","What SignalR adds",[10,231,232],{},"SignalR uses that \"pipe\" to implement an RPC protocol. Instead of sending plain text, it sends messages with meaning. That makes calling a remote method as simple as:",[42,234,238],{"className":235,"code":236,"language":237,"meta":51,"style":51},"language-csharp shiki shiki-themes github-dark","await hubConnection.InvokeAsync(\"SendMessage\", message);\n","csharp",[49,239,240],{"__ignoreMap":51},[88,241,242,246,249,253,256,260],{"class":90,"line":91},[88,243,245],{"class":244},"snl16","await",[88,247,248],{"class":94}," hubConnection.",[88,250,252],{"class":251},"svObZ","InvokeAsync",[88,254,255],{"class":94},"(",[88,257,259],{"class":258},"sU2Wk","\"SendMessage\"",[88,261,262],{"class":94},", message);\n",[10,264,265],{},"But there's actually a fair amount of protocol behind it.",[27,267,269],{"id":268},"how-a-signalr-connection-works","How a SignalR connection works",[10,271,272],{},"The connection has three phases:",[274,275,276,296,306],"ol",{},[59,277,278,283,284,287,288,291,292,295],{},[34,279,280],{},[49,281,282],{},"negotiate",": the client makes a ",[49,285,286],{},"POST"," to ",[49,289,290],{},"\u002Fhub\u002Fnegotiate"," and receives a temporary ",[49,293,294],{},"connectionToken",".",[59,297,298,302,303,295],{},[34,299,300],{},[49,301,36],{},": the client opens the WebSocket connection using that token: ",[49,304,305],{},"ws:\u002F\u002F{host}\u002Fhub?id={connectionToken}",[59,307,308,313],{},[34,309,310],{},[49,311,312],{},"handshake",": the first message sent over the socket is a JSON telling SignalR which protocol the client will use.",[42,315,317],{"className":82,"code":316,"language":84,"meta":51,"style":51},"sequenceDiagram\n  Note over Client,Server: 1. negotiate\n  Client->>Server: POST \u002Fhub\u002Fnegotiate\n  Server-->>Client: connectionToken\n\n  Note over Client,Server: 2. WebSocket\n  Client->>Server: ws:\u002F\u002F{host}\u002Fhub?id={connectionToken}\n  Server-->>Client: 101 Switching Protocols\n\n  Note over Client,Server: 3. handshake\n  Client->>Server: JSON {\"protocol\":\"json\",\"version\":1}\n  Server-->>Client: {}\n\n  Note over Client,Server: Connection established\n",[49,318,319,324,329,335,341,348,354,360,366,371,377,383,389,394],{"__ignoreMap":51},[88,320,321],{"class":90,"line":91},[88,322,323],{"class":94},"sequenceDiagram\n",[88,325,326],{"class":90,"line":98},[88,327,328],{"class":94},"  Note over Client,Server: 1. negotiate\n",[88,330,332],{"class":90,"line":331},3,[88,333,334],{"class":94},"  Client->>Server: POST \u002Fhub\u002Fnegotiate\n",[88,336,338],{"class":90,"line":337},4,[88,339,340],{"class":94},"  Server-->>Client: connectionToken\n",[88,342,344],{"class":90,"line":343},5,[88,345,347],{"emptyLinePlaceholder":346},true,"\n",[88,349,351],{"class":90,"line":350},6,[88,352,353],{"class":94},"  Note over Client,Server: 2. WebSocket\n",[88,355,357],{"class":90,"line":356},7,[88,358,359],{"class":94},"  Client->>Server: ws:\u002F\u002F{host}\u002Fhub?id={connectionToken}\n",[88,361,363],{"class":90,"line":362},8,[88,364,365],{"class":94},"  Server-->>Client: 101 Switching Protocols\n",[88,367,369],{"class":90,"line":368},9,[88,370,347],{"emptyLinePlaceholder":346},[88,372,374],{"class":90,"line":373},10,[88,375,376],{"class":94},"  Note over Client,Server: 3. handshake\n",[88,378,380],{"class":90,"line":379},11,[88,381,382],{"class":94},"  Client->>Server: JSON {\"protocol\":\"json\",\"version\":1}\n",[88,384,386],{"class":90,"line":385},12,[88,387,388],{"class":94},"  Server-->>Client: {}\n",[88,390,392],{"class":90,"line":391},13,[88,393,347],{"emptyLinePlaceholder":346},[88,395,397],{"class":90,"line":396},14,[88,398,399],{"class":94},"  Note over Client,Server: Connection established\n",[10,401,402],{},"Only after the handshake does the hub consider the connection established.",[10,404,405],{},"These last two phases are exactly what make testing SignalR applications more challenging than just testing a WebSocket. SignalR is not just a WebSocket: it's a complete protocol on top of the WebSocket.",[27,407,409],{"id":408},"the-signalr-handshake","The SignalR handshake",[10,411,412,413,415],{},"Right after opening the WebSocket, the client sends a SignalR handshake message. In other words: opening a WebSocket to a SignalR endpoint is not enough. SignalR expects to receive a specific ",[112,414,312],{}," right after the connection. Without it, the connection stays open but the hub never considers it \"ready\".",[42,417,421],{"className":418,"code":419,"language":420,"meta":51,"style":51},"language-json shiki shiki-themes github-dark","{\n  \"protocol\": \"json\",\n  \"version\": 1\n}\n","json",[49,422,423,428,443,453],{"__ignoreMap":51},[88,424,425],{"class":90,"line":91},[88,426,427],{"class":94},"{\n",[88,429,430,434,437,440],{"class":90,"line":98},[88,431,433],{"class":432},"sDLfK","  \"protocol\"",[88,435,436],{"class":94},": ",[88,438,439],{"class":258},"\"json\"",[88,441,442],{"class":94},",\n",[88,444,445,448,450],{"class":90,"line":331},[88,446,447],{"class":432},"  \"version\"",[88,449,436],{"class":94},[88,451,452],{"class":432},"1\n",[88,454,455],{"class":90,"line":337},[88,456,457],{"class":94},"}\n",[10,459,460,461,464,465,468],{},"In practice, it's sent on a single line, terminated by the ",[112,462,463],{},"record separator"," (",[49,466,467],{},"␞","):",[42,470,472],{"className":418,"code":471,"language":420,"meta":51,"style":51},"{\"protocol\":\"json\",\"version\":1}␞\n",[49,473,474],{"__ignoreMap":51},[88,475,476,479,482,485,487,490,493,495,498],{"class":90,"line":91},[88,477,478],{"class":94},"{",[88,480,481],{"class":432},"\"protocol\"",[88,483,484],{"class":94},":",[88,486,439],{"class":258},[88,488,489],{"class":94},",",[88,491,492],{"class":432},"\"version\"",[88,494,484],{"class":94},[88,496,497],{"class":432},"1",[88,499,500],{"class":94},"}␞\n",[56,502,503,515],{},[59,504,505,508,509,511,512],{},[49,506,507],{},"protocol"," - Serialization protocol used, either ",[49,510,420],{}," or ",[49,513,514],{},"messagepack",[59,516,517,520],{},[49,518,519],{},"version"," - SignalR protocol version",[10,522,523],{},"If the server accepts the protocol, it replies with an empty JSON, also terminated by a record separator:",[42,525,527],{"className":418,"code":526,"language":420,"meta":51,"style":51},"{}␞\n",[49,528,529],{"__ignoreMap":51},[88,530,531],{"class":90,"line":91},[88,532,526],{"class":94},[10,534,535],{},"Without the handshake response, the connection is not considered established.",[10,537,538],{},"But the payload actually sent over the wire is:",[42,540,543],{"className":541,"code":542,"language":47},[45],"7b2270726f746f636f6c223a226a736f6e222c2276657273696f6e223a317d1e\n",[49,544,542],{"__ignoreMap":51},[125,546,547,557],{},[128,548,549],{},[131,550,551,554],{},[134,552,553],{},"Hex",[134,555,556],{},"Chars",[140,558,559,570,582,593,604,615,626,637,648,660],{},[131,560,561,566],{},[145,562,563],{},[49,564,565],{},"7b",[145,567,568],{},[49,569,478],{},[131,571,572,577],{},[145,573,574],{},[49,575,576],{},"22",[145,578,579],{},[49,580,581],{},"\"",[131,583,584,589],{},[145,585,586],{},[49,587,588],{},"70 72 6f 74 6f 63 6f 6c",[145,590,591],{},[49,592,507],{},[131,594,595,600],{},[145,596,597],{},[49,598,599],{},"3a",[145,601,602],{},[49,603,484],{},[131,605,606,611],{},[145,607,608],{},[49,609,610],{},"6a 73 6f 6e",[145,612,613],{},[49,614,420],{},[131,616,617,622],{},[145,618,619],{},[49,620,621],{},"2c",[145,623,624],{},[49,625,489],{},[131,627,628,633],{},[145,629,630],{},[49,631,632],{},"76 65 72 73 69 6f 6e",[145,634,635],{},[49,636,519],{},[131,638,639,644],{},[145,640,641],{},[49,642,643],{},"31",[145,645,646],{},[49,647,497],{},[131,649,650,655],{},[145,651,652],{},[49,653,654],{},"7d",[145,656,657],{},[49,658,659],{},"}",[131,661,662,667],{},[145,663,664],{},[49,665,666],{},"1e",[145,668,669],{},[49,670,671],{},"0x1E",[10,673,674,675,295],{},"If you look closely, you'll see the message ends with a special byte, called the ",[112,676,677],{},"Record Separator",[22,679,677],{"id":680},"record-separator",[10,682,683],{},"It's the delimiter used by the SignalR protocol. It's what tells SignalR where a message ends. Without it, SignalR receives text but can't tell where the first message ends, and the handshake never completes.",[27,685,687],{"id":686},"why-does-the-record-separator-exist","Why does the Record Separator exist?",[10,689,690],{},"On a WebSocket, several messages can arrive in a single frame. Without a delimiter, they're indistinguishable:",[42,692,695],{"className":693,"code":694,"language":47},[45],"Message1Message2Message3\n",[49,696,694],{"__ignoreMap":51},[10,698,699,700,464,702,468],{},"So SignalR separates each message with the byte ",[49,701,671],{},[49,703,467],{},[42,705,708],{"className":706,"code":707,"language":47},[45],"Message1␞Message2␞Message3␞\n",[49,709,707],{"__ignoreMap":51},[10,711,712,713,715],{},"That way, the ",[49,714,467],{}," marks the end of each message.",[22,717,719],{"id":718},"how-to-view-signalr-messages","How to view SignalR messages",[10,721,722],{},"There are several ways, but let's use the simplest one: open the browser DevTools and watch the WebSocket traffic.",[10,724,725],{},[726,727],"img",{"alt":728,"src":729},"DevTools comparison between a raw WebSocket connection and a SignalR connection","\u002Fblog\u002Fsignalr-trap-not-just-websockets-devtool.png",[10,731,732,733,735],{},"As you can see in the image, on the left we have a raw WebSocket connection, carrying only the application's final data. On the right, a SignalR connection, where you can see handshake messages, remote method invocations, and ping\u002Fpong messages. Notice too that SignalR messages follow a different payload structure, with the ",[49,734,671],{}," delimiter at the end of each message.",[22,737,739],{"id":738},"anatomy-of-a-signalr-message","Anatomy of a SignalR message",[10,741,742,743,746],{},"SignalR messages are identified by the ",[49,744,745],{},"type"," field, which determines the structure and meaning of the message. The main types are:",[125,748,749,762],{},[128,750,751],{},[131,752,753,756,759],{},[134,754,755],{},"Type",[134,757,758],{},"Name",[134,760,761],{},"Function",[140,763,764,774,785,796,807,818,829],{},[131,765,766,768,771],{},[145,767,497],{},[145,769,770],{},"Invocation",[145,772,773],{},"Invoke a remote method and wait for a response",[131,775,776,779,782],{},[145,777,778],{},"2",[145,780,781],{},"StreamItem",[145,783,784],{},"Send an item from a data stream",[131,786,787,790,793],{},[145,788,789],{},"3",[145,791,792],{},"Completion",[145,794,795],{},"Complete a stream or return a final result",[131,797,798,801,804],{},[145,799,800],{},"4",[145,802,803],{},"StreamInvocation",[145,805,806],{},"Invoke a remote method that returns a stream",[131,808,809,812,815],{},[145,810,811],{},"5",[145,813,814],{},"CancelInvocation",[145,816,817],{},"Cancel a stream in progress",[131,819,820,823,826],{},[145,821,822],{},"6",[145,824,825],{},"Ping",[145,827,828],{},"Keep the connection alive (keep-alive)",[131,830,831,834,837],{},[145,832,833],{},"7",[145,835,836],{},"Close",[145,838,839],{},"Close the connection gracefully",[27,841,843],{"id":842},"example-remote-method-invocation","Example: remote method invocation",[10,845,846],{},"A simple invocation example:",[42,848,850],{"className":418,"code":849,"language":420,"meta":51,"style":51},"{\n  \"type\": 1,\n  \"target\": \"ReceiveMessage\",\n  \"arguments\": [\n    \"Hello\"\n  ]\n}\n",[49,851,852,856,867,879,887,892,897],{"__ignoreMap":51},[88,853,854],{"class":90,"line":91},[88,855,427],{"class":94},[88,857,858,861,863,865],{"class":90,"line":98},[88,859,860],{"class":432},"  \"type\"",[88,862,436],{"class":94},[88,864,497],{"class":432},[88,866,442],{"class":94},[88,868,869,872,874,877],{"class":90,"line":331},[88,870,871],{"class":432},"  \"target\"",[88,873,436],{"class":94},[88,875,876],{"class":258},"\"ReceiveMessage\"",[88,878,442],{"class":94},[88,880,881,884],{"class":90,"line":337},[88,882,883],{"class":432},"  \"arguments\"",[88,885,886],{"class":94},": [\n",[88,888,889],{"class":90,"line":343},[88,890,891],{"class":258},"    \"Hello\"\n",[88,893,894],{"class":90,"line":350},[88,895,896],{"class":94},"  ]\n",[88,898,899],{"class":90,"line":356},[88,900,457],{"class":94},[10,902,903,904,484],{},"Which, on the wire, is sent on a single line, terminated by the ",[112,905,463],{},[42,907,909],{"className":418,"code":908,"language":420,"meta":51,"style":51},"{\"type\":1,\"target\":\"ReceiveMessage\",\"arguments\":[\"Hello\"]}␞\n",[49,910,911],{"__ignoreMap":51},[88,912,913,915,918,920,922,924,927,929,931,933,936,939,942],{"class":90,"line":91},[88,914,478],{"class":94},[88,916,917],{"class":432},"\"type\"",[88,919,484],{"class":94},[88,921,497],{"class":432},[88,923,489],{"class":94},[88,925,926],{"class":432},"\"target\"",[88,928,484],{"class":94},[88,930,876],{"class":258},[88,932,489],{"class":94},[88,934,935],{"class":432},"\"arguments\"",[88,937,938],{"class":94},":[",[88,940,941],{"class":258},"\"Hello\"",[88,943,944],{"class":94},"]}␞\n",[56,946,947,958,964],{},[59,948,949,951,952,954,955,957],{},[49,950,745],{}," - Type ",[49,953,497],{},": remote method invocation (",[112,956,770],{},")",[59,959,960,963],{},[49,961,962],{},"target"," - Name of the remote method to invoke",[59,965,966,969],{},[49,967,968],{},"arguments"," - Arguments sent to the remote method",[27,971,973],{"id":972},"example-keep-alive-ping","Example: Keep-alive (Ping)",[10,975,976],{},"A ping just to keep the connection alive:",[42,978,979],{"className":418,"code":526,"language":420,"meta":51,"style":51},[49,980,981],{"__ignoreMap":51},[88,982,983],{"class":90,"line":91},[88,984,526],{"class":94},[10,986,987],{},"Or explicitly:",[42,989,991],{"className":418,"code":990,"language":420,"meta":51,"style":51},"{\"type\":6}␞\n",[49,992,993],{"__ignoreMap":51},[88,994,995,997,999,1001,1003],{"class":90,"line":91},[88,996,478],{"class":94},[88,998,917],{"class":432},[88,1000,484],{"class":94},[88,1002,822],{"class":432},[88,1004,500],{"class":94},[10,1006,1007],{},"The server replies with a ping the same way to confirm the connection.",[22,1009,1011],{"id":1010},"the-main-friction-points-when-testing-signalr","The main friction points when testing SignalR",[10,1013,1014],{},"Before we get to the tools, it's worth summarizing the main friction points that make testing SignalR harder:",[56,1016,1017,1023,1038,1044],{},[59,1018,1019,1022],{},[34,1020,1021],{},"It's not a single HTTP request."," It's a sequence: negotiate (HTTP) → WebSocket → handshake (a message inside the socket). Traditional HTTP testing tools don't cover this natively.",[59,1024,1025,1028,1029,1031,1032,1034,1035,295],{},[34,1026,1027],{},"The negotiation token has a limited lifetime."," The ",[49,1030,294],{}," maps to a connection created by the server during ",[49,1033,282],{},"; if you take too long to open the WebSocket, that connection no longer exists and the server returns ",[49,1036,1037],{},"404",[59,1039,1040,1043],{},[34,1041,1042],{},"The WebSocket connection on its own isn't enough."," A lot of people open the socket, don't send the handshake, and assume SignalR \"isn't working\", when it's actually waiting for the first message.",[59,1045,1046,1052],{},[34,1047,1048,1049,1051],{},"The ",[49,1050,467],{}," delimiter is easy to forget",", especially when writing the payload by hand or generating messages programmatically without the official client.",[22,1054,1056],{"id":1055},"how-to-test-signalr-without-the-official-client","How to test SignalR without the official client",[10,1058,1059],{},"I'll show two tools that are widely used during development: Postman, for manual tests, and k6, for load tests. Both are great, but neither has native support for SignalR. That means you have to build the SignalR protocol on top of the WebSocket by hand.",[27,1061,1063],{"id":1062},"manual-tests-with-postman","Manual tests with Postman",[10,1065,1066],{},"Recent versions of Postman support WebSockets, but only WebSocket. It can open the connection, but it doesn't know about:",[56,1068,1069,1071,1073,1076],{},[59,1070,282],{},[59,1072,312],{},[59,1074,1075],{},"protocol framing",[59,1077,1078],{},"invocations",[10,1080,1081],{},"You have to build everything by hand. To test the full flow, including the WebSocket:",[1083,1084,1086],"h4",{"id":1085},"_1-negotiate-negotiate-protocol-and-transport","1. Negotiate (negotiate protocol and transport)",[10,1088,1089,1092,1093,1095],{},[49,1090,1091],{},"POST: http:\u002F\u002Flocalhost:8080\u002Fdemo-hub\u002Fnegotiate?negotiateVersion=1"," and copy the ",[49,1094,294],{}," from the response.",[1097,1098,1099,1109,1122],"blockquote",{},[10,1100,1101,1108],{},[34,1102,1103,1104,1107],{},"What is ",[49,1105,1106],{},"negotiateVersion=1","?","\nIt's the version of the negotiation protocol (not to be confused with the SignalR protocol version in the handshake).",[56,1110,1111,1117],{},[59,1112,1113,1116],{},[49,1114,1115],{},"negotiateVersion=0",": legacy format, supports only one transport per negotiation",[59,1118,1119,1121],{},[49,1120,1106],{},": current format, supports multiple transports in the same negotiation",[10,1123,1124,1125,1127],{},"Always use ",[49,1126,1106],{}," to get full fallback support (WebSocket → ServerSentEvents → LongPolling).",[42,1129,1131],{"className":418,"code":1130,"language":420,"meta":51,"style":51},"{\n  \"negotiateVersion\": 1,\n  \"connectionId\": \"fdmsZsc0A3ejTY5FI0r0NA\",\n  \"connectionToken\": \"3lWE4OAsva6TLb2CcJcoaA\",\n  \"availableTransports\":\n  [\n    { \"transport\": \"WebSockets\", \"transferFormats\": [\"Text\", \"Binary\"] },\n    { \"transport\": \"ServerSentEvents\", \"transferFormats\": [\"Text\"] },\n    { \"transport\": \"LongPolling\", \"transferFormats\": [\"Text\", \"Binary\"] }\n  ]\n}\n",[49,1132,1133,1137,1148,1160,1172,1180,1185,1218,1239,1265,1269],{"__ignoreMap":51},[88,1134,1135],{"class":90,"line":91},[88,1136,427],{"class":94},[88,1138,1139,1142,1144,1146],{"class":90,"line":98},[88,1140,1141],{"class":432},"  \"negotiateVersion\"",[88,1143,436],{"class":94},[88,1145,497],{"class":432},[88,1147,442],{"class":94},[88,1149,1150,1153,1155,1158],{"class":90,"line":331},[88,1151,1152],{"class":432},"  \"connectionId\"",[88,1154,436],{"class":94},[88,1156,1157],{"class":258},"\"fdmsZsc0A3ejTY5FI0r0NA\"",[88,1159,442],{"class":94},[88,1161,1162,1165,1167,1170],{"class":90,"line":337},[88,1163,1164],{"class":432},"  \"connectionToken\"",[88,1166,436],{"class":94},[88,1168,1169],{"class":258},"\"3lWE4OAsva6TLb2CcJcoaA\"",[88,1171,442],{"class":94},[88,1173,1174,1177],{"class":90,"line":343},[88,1175,1176],{"class":432},"  \"availableTransports\"",[88,1178,1179],{"class":94},":\n",[88,1181,1182],{"class":90,"line":350},[88,1183,1184],{"class":94},"  [\n",[88,1186,1187,1190,1193,1195,1198,1201,1204,1207,1210,1212,1215],{"class":90,"line":356},[88,1188,1189],{"class":94},"    { ",[88,1191,1192],{"class":432},"\"transport\"",[88,1194,436],{"class":94},[88,1196,1197],{"class":258},"\"WebSockets\"",[88,1199,1200],{"class":94},", ",[88,1202,1203],{"class":432},"\"transferFormats\"",[88,1205,1206],{"class":94},": [",[88,1208,1209],{"class":258},"\"Text\"",[88,1211,1200],{"class":94},[88,1213,1214],{"class":258},"\"Binary\"",[88,1216,1217],{"class":94},"] },\n",[88,1219,1220,1222,1224,1226,1229,1231,1233,1235,1237],{"class":90,"line":362},[88,1221,1189],{"class":94},[88,1223,1192],{"class":432},[88,1225,436],{"class":94},[88,1227,1228],{"class":258},"\"ServerSentEvents\"",[88,1230,1200],{"class":94},[88,1232,1203],{"class":432},[88,1234,1206],{"class":94},[88,1236,1209],{"class":258},[88,1238,1217],{"class":94},[88,1240,1241,1243,1245,1247,1250,1252,1254,1256,1258,1260,1262],{"class":90,"line":368},[88,1242,1189],{"class":94},[88,1244,1192],{"class":432},[88,1246,436],{"class":94},[88,1248,1249],{"class":258},"\"LongPolling\"",[88,1251,1200],{"class":94},[88,1253,1203],{"class":432},[88,1255,1206],{"class":94},[88,1257,1209],{"class":258},[88,1259,1200],{"class":94},[88,1261,1214],{"class":258},[88,1263,1264],{"class":94},"] }\n",[88,1266,1267],{"class":90,"line":373},[88,1268,896],{"class":94},[88,1270,1271],{"class":90,"line":379},[88,1272,457],{"class":94},[1097,1274,1275,1286,1302],{},[10,1276,1277],{},[34,1278,1279,1280,1283,1284,484],{},"Difference between ",[49,1281,1282],{},"connectionId"," and ",[49,1285,294],{},[56,1287,1288,1295],{},[59,1289,1290,1294],{},[34,1291,1292],{},[49,1293,1282],{},": a readable identifier for the connection, used to send messages to a specific connection, logging, debugging, etc. (e.g. \"fdmsZsc0A3ejTY5FI0r0NA\")",[59,1296,1297,1301],{},[34,1298,1299],{},[49,1300,294],{},": a temporary authentication token, required to open the WebSocket (e.g. \"3lWE4OAsva6TLb2CcJcoaA\")",[10,1303,1304,1305,1307,1308,295],{},"Always use the ",[49,1306,294],{}," on the WebSocket, not the ",[49,1309,1282],{},[1083,1311,1313],{"id":1312},"_2-websocket-open-the-connection","2. WebSocket (open the connection)",[10,1315,1316,1317,287,1320,295],{},"Open a new ",[34,1318,1319],{},"WebSocket Request",[49,1321,1322],{},"ws:\u002F\u002Flocalhost:8080\u002Fdemo-hub?id={{connectionToken}}",[1083,1324,1326],{"id":1325},"_3-handshake-send-the-protocol","3. Handshake (send the protocol)",[56,1328,1329,1335],{},[59,1330,1331,1332,295],{},"As soon as the socket connects, send this as the first message: ",[49,1333,1334],{},"{\"protocol\":\"json\",\"version\":1}␞",[59,1336,1337],{},"If the handshake is accepted, you're ready to send and receive SignalR messages.",[10,1339,1340],{},[726,1341],{"alt":1342,"src":1343},"WebSocket request in Postman sending the SignalR handshake","\u002Fblog\u002Fsignalr-trap-not-just-websockets-postman.png",[27,1345,1347],{"id":1346},"load-tests-with-k6","Load tests with k6",[10,1349,1350],{},"k6 has WebSocket support and can hold thousands of concurrent connections, which makes it a great base for load testing applications.",[10,1352,1353],{},"The challenge is exactly the same as with Postman: k6 knows WebSockets, but it doesn't know SignalR.",[10,1355,1356,1357,1360],{},"For real load, with many simultaneous connections, each with its own negotiate → WebSocket → handshake cycle, k6's plain HTTP approach isn't enough; you need the ",[49,1358,1359],{},"k6\u002Fws"," module to open the socket.",[10,1362,1363],{},"Here's an example script with those steps implemented:",[42,1365,1369],{"className":1366,"code":1367,"language":1368,"meta":51,"style":51},"language-typescript shiki shiki-themes github-dark","import { check, sleep } from 'k6';\nimport http from 'k6\u002Fhttp';\nimport ws from 'k6\u002Fws';\n\nconst API_HOST = __ENV.API_HOST || 'http:\u002F\u002Flocalhost:8080';\nconst DURATION = parseInt(__ENV.DURATION || '1000');\nconst WS_HOST = API_HOST.replace(\u002F^http\u002F, 'ws');\n\n\u002F\u002F SignalR frames are delimited by ASCII record separator (0x1e)\nconst RS = String.fromCharCode(30);\n\nexport default () => {\n  const negotiateRes = http.post(`${API_HOST}\u002Fdemo-hub\u002Fnegotiate?negotiateVersion=1`);\n  check(negotiateRes, { 'negotiate: status 200': (r) => r.status === 200 });\n\n  const { connectionId, connectionToken } = JSON.parse(negotiateRes.body);\n\n  let handshakeOk = false;\n  let messagesReceived = 0;\n\n  const wsRes = ws.connect(\n    `${WS_HOST}\u002Fdemo-hub?id=${encodeURIComponent(connectionToken)}`,\n    null,\n    (socket) => {\n      socket.on('open', () => {\n        socket.send(`{\"protocol\":\"json\",\"version\":1}${RS}`);\n      });\n\n      socket.on('message', (data) => {\n        \u002F\u002F Loop through all frames in the message (there may be multiple frames in a single message)\n        for (const frame of data.split(RS).filter(Boolean)) {\n          const msg = JSON.parse(frame); \u002F\u002F parse the frame as JSON\n          if (Object.keys(msg).length === 0) {\n            \u002F\u002F handshake ack, trigger the demo stream targeted at this connection\n            handshakeOk = true;\n            http.get(`${API_HOST}\u002Fdemo?duration=${DURATION}&connectionId=${connectionId}`);\n          } else if (msg.type === 1) {\n            messagesReceived++;\n          }\n        }\n      });\n\n      socket.setTimeout(() => socket.close(), 2000);\n    }\n  );\n\n  check(wsRes, { 'ws: upgraded (101)': (r) => r && r.status === 101 });\n  check({ handshakeOk, messagesReceived }, {\n    'ws: handshake completed': (r) => r.handshakeOk,\n    'ws: received streamed messages': (r) => r.messagesReceived > 0,\n  });\n\n  sleep(1);\n}\n","typescript",[49,1370,1371,1388,1402,1416,1420,1445,1471,1508,1512,1518,1540,1544,1561,1589,1624,1629,1660,1665,1681,1696,1701,1720,1746,1754,1769,1790,1811,1817,1822,1846,1852,1887,1910,1936,1942,1955,1984,2006,2017,2023,2029,2034,2039,2066,2072,2078,2083,2117,2125,2142,2166,2172,2177,2189],{"__ignoreMap":51},[88,1372,1373,1376,1379,1382,1385],{"class":90,"line":91},[88,1374,1375],{"class":244},"import",[88,1377,1378],{"class":94}," { check, sleep } ",[88,1380,1381],{"class":244},"from",[88,1383,1384],{"class":258}," 'k6'",[88,1386,1387],{"class":94},";\n",[88,1389,1390,1392,1395,1397,1400],{"class":90,"line":98},[88,1391,1375],{"class":244},[88,1393,1394],{"class":94}," http ",[88,1396,1381],{"class":244},[88,1398,1399],{"class":258}," 'k6\u002Fhttp'",[88,1401,1387],{"class":94},[88,1403,1404,1406,1409,1411,1414],{"class":90,"line":331},[88,1405,1375],{"class":244},[88,1407,1408],{"class":94}," ws ",[88,1410,1381],{"class":244},[88,1412,1413],{"class":258}," 'k6\u002Fws'",[88,1415,1387],{"class":94},[88,1417,1418],{"class":90,"line":337},[88,1419,347],{"emptyLinePlaceholder":346},[88,1421,1422,1425,1428,1431,1434,1437,1440,1443],{"class":90,"line":343},[88,1423,1424],{"class":244},"const",[88,1426,1427],{"class":432}," API_HOST",[88,1429,1430],{"class":244}," =",[88,1432,1433],{"class":94}," __ENV.",[88,1435,1436],{"class":432},"API_HOST",[88,1438,1439],{"class":244}," ||",[88,1441,1442],{"class":258}," 'http:\u002F\u002Flocalhost:8080'",[88,1444,1387],{"class":94},[88,1446,1447,1449,1452,1454,1457,1460,1463,1465,1468],{"class":90,"line":350},[88,1448,1424],{"class":244},[88,1450,1451],{"class":432}," DURATION",[88,1453,1430],{"class":244},[88,1455,1456],{"class":251}," parseInt",[88,1458,1459],{"class":94},"(__ENV.",[88,1461,1462],{"class":432},"DURATION",[88,1464,1439],{"class":244},[88,1466,1467],{"class":258}," '1000'",[88,1469,1470],{"class":94},");\n",[88,1472,1473,1475,1478,1480,1482,1484,1487,1489,1492,1495,1499,1501,1503,1506],{"class":90,"line":356},[88,1474,1424],{"class":244},[88,1476,1477],{"class":432}," WS_HOST",[88,1479,1430],{"class":244},[88,1481,1427],{"class":432},[88,1483,295],{"class":94},[88,1485,1486],{"class":251},"replace",[88,1488,255],{"class":94},[88,1490,1491],{"class":258},"\u002F",[88,1493,1494],{"class":244},"^",[88,1496,1498],{"class":1497},"sns5M","http",[88,1500,1491],{"class":258},[88,1502,1200],{"class":94},[88,1504,1505],{"class":258},"'ws'",[88,1507,1470],{"class":94},[88,1509,1510],{"class":90,"line":362},[88,1511,347],{"emptyLinePlaceholder":346},[88,1513,1514],{"class":90,"line":368},[88,1515,1517],{"class":1516},"sAwPA","\u002F\u002F SignalR frames are delimited by ASCII record separator (0x1e)\n",[88,1519,1520,1522,1525,1527,1530,1533,1535,1538],{"class":90,"line":373},[88,1521,1424],{"class":244},[88,1523,1524],{"class":432}," RS",[88,1526,1430],{"class":244},[88,1528,1529],{"class":94}," String.",[88,1531,1532],{"class":251},"fromCharCode",[88,1534,255],{"class":94},[88,1536,1537],{"class":432},"30",[88,1539,1470],{"class":94},[88,1541,1542],{"class":90,"line":379},[88,1543,347],{"emptyLinePlaceholder":346},[88,1545,1546,1549,1552,1555,1558],{"class":90,"line":385},[88,1547,1548],{"class":244},"export",[88,1550,1551],{"class":244}," default",[88,1553,1554],{"class":94}," () ",[88,1556,1557],{"class":244},"=>",[88,1559,1560],{"class":94}," {\n",[88,1562,1563,1566,1569,1571,1574,1577,1579,1582,1584,1587],{"class":90,"line":391},[88,1564,1565],{"class":244},"  const",[88,1567,1568],{"class":432}," negotiateRes",[88,1570,1430],{"class":244},[88,1572,1573],{"class":94}," http.",[88,1575,1576],{"class":251},"post",[88,1578,255],{"class":94},[88,1580,1581],{"class":258},"`${",[88,1583,1436],{"class":432},[88,1585,1586],{"class":258},"}\u002Fdemo-hub\u002Fnegotiate?negotiateVersion=1`",[88,1588,1470],{"class":94},[88,1590,1591,1594,1597,1600,1603,1607,1610,1612,1615,1618,1621],{"class":90,"line":396},[88,1592,1593],{"class":251},"  check",[88,1595,1596],{"class":94},"(negotiateRes, { ",[88,1598,1599],{"class":258},"'negotiate: status 200'",[88,1601,1602],{"class":94},": (",[88,1604,1606],{"class":1605},"s9osk","r",[88,1608,1609],{"class":94},") ",[88,1611,1557],{"class":244},[88,1613,1614],{"class":94}," r.status ",[88,1616,1617],{"class":244},"===",[88,1619,1620],{"class":432}," 200",[88,1622,1623],{"class":94}," });\n",[88,1625,1627],{"class":90,"line":1626},15,[88,1628,347],{"emptyLinePlaceholder":346},[88,1630,1632,1634,1637,1639,1641,1643,1646,1649,1652,1654,1657],{"class":90,"line":1631},16,[88,1633,1565],{"class":244},[88,1635,1636],{"class":94}," { ",[88,1638,1282],{"class":432},[88,1640,1200],{"class":94},[88,1642,294],{"class":432},[88,1644,1645],{"class":94}," } ",[88,1647,1648],{"class":244},"=",[88,1650,1651],{"class":432}," JSON",[88,1653,295],{"class":94},[88,1655,1656],{"class":251},"parse",[88,1658,1659],{"class":94},"(negotiateRes.body);\n",[88,1661,1663],{"class":90,"line":1662},17,[88,1664,347],{"emptyLinePlaceholder":346},[88,1666,1668,1671,1674,1676,1679],{"class":90,"line":1667},18,[88,1669,1670],{"class":244},"  let",[88,1672,1673],{"class":94}," handshakeOk ",[88,1675,1648],{"class":244},[88,1677,1678],{"class":432}," false",[88,1680,1387],{"class":94},[88,1682,1684,1686,1689,1691,1694],{"class":90,"line":1683},19,[88,1685,1670],{"class":244},[88,1687,1688],{"class":94}," messagesReceived ",[88,1690,1648],{"class":244},[88,1692,1693],{"class":432}," 0",[88,1695,1387],{"class":94},[88,1697,1699],{"class":90,"line":1698},20,[88,1700,347],{"emptyLinePlaceholder":346},[88,1702,1704,1706,1709,1711,1714,1717],{"class":90,"line":1703},21,[88,1705,1565],{"class":244},[88,1707,1708],{"class":432}," wsRes",[88,1710,1430],{"class":244},[88,1712,1713],{"class":94}," ws.",[88,1715,1716],{"class":251},"connect",[88,1718,1719],{"class":94},"(\n",[88,1721,1723,1726,1729,1732,1735,1737,1739,1741,1744],{"class":90,"line":1722},22,[88,1724,1725],{"class":258},"    `${",[88,1727,1728],{"class":432},"WS_HOST",[88,1730,1731],{"class":258},"}\u002Fdemo-hub?id=${",[88,1733,1734],{"class":251},"encodeURIComponent",[88,1736,255],{"class":258},[88,1738,294],{"class":94},[88,1740,957],{"class":258},[88,1742,1743],{"class":258},"}`",[88,1745,442],{"class":94},[88,1747,1749,1752],{"class":90,"line":1748},23,[88,1750,1751],{"class":432},"    null",[88,1753,442],{"class":94},[88,1755,1757,1760,1763,1765,1767],{"class":90,"line":1756},24,[88,1758,1759],{"class":94},"    (",[88,1761,1762],{"class":1605},"socket",[88,1764,1609],{"class":94},[88,1766,1557],{"class":244},[88,1768,1560],{"class":94},[88,1770,1772,1775,1778,1780,1783,1786,1788],{"class":90,"line":1771},25,[88,1773,1774],{"class":94},"      socket.",[88,1776,1777],{"class":251},"on",[88,1779,255],{"class":94},[88,1781,1782],{"class":258},"'open'",[88,1784,1785],{"class":94},", () ",[88,1787,1557],{"class":244},[88,1789,1560],{"class":94},[88,1791,1793,1796,1799,1801,1804,1807,1809],{"class":90,"line":1792},26,[88,1794,1795],{"class":94},"        socket.",[88,1797,1798],{"class":251},"send",[88,1800,255],{"class":94},[88,1802,1803],{"class":258},"`{\"protocol\":\"json\",\"version\":1}${",[88,1805,1806],{"class":432},"RS",[88,1808,1743],{"class":258},[88,1810,1470],{"class":94},[88,1812,1814],{"class":90,"line":1813},27,[88,1815,1816],{"class":94},"      });\n",[88,1818,1820],{"class":90,"line":1819},28,[88,1821,347],{"emptyLinePlaceholder":346},[88,1823,1825,1827,1829,1831,1834,1837,1840,1842,1844],{"class":90,"line":1824},29,[88,1826,1774],{"class":94},[88,1828,1777],{"class":251},[88,1830,255],{"class":94},[88,1832,1833],{"class":258},"'message'",[88,1835,1836],{"class":94},", (",[88,1838,1839],{"class":1605},"data",[88,1841,1609],{"class":94},[88,1843,1557],{"class":244},[88,1845,1560],{"class":94},[88,1847,1849],{"class":90,"line":1848},30,[88,1850,1851],{"class":1516},"        \u002F\u002F Loop through all frames in the message (there may be multiple frames in a single message)\n",[88,1853,1855,1858,1860,1862,1865,1868,1871,1874,1876,1878,1881,1884],{"class":90,"line":1854},31,[88,1856,1857],{"class":244},"        for",[88,1859,464],{"class":94},[88,1861,1424],{"class":244},[88,1863,1864],{"class":432}," frame",[88,1866,1867],{"class":244}," of",[88,1869,1870],{"class":94}," data.",[88,1872,1873],{"class":251},"split",[88,1875,255],{"class":94},[88,1877,1806],{"class":432},[88,1879,1880],{"class":94},").",[88,1882,1883],{"class":251},"filter",[88,1885,1886],{"class":94},"(Boolean)) {\n",[88,1888,1890,1893,1896,1898,1900,1902,1904,1907],{"class":90,"line":1889},32,[88,1891,1892],{"class":244},"          const",[88,1894,1895],{"class":432}," msg",[88,1897,1430],{"class":244},[88,1899,1651],{"class":432},[88,1901,295],{"class":94},[88,1903,1656],{"class":251},[88,1905,1906],{"class":94},"(frame); ",[88,1908,1909],{"class":1516},"\u002F\u002F parse the frame as JSON\n",[88,1911,1913,1916,1919,1922,1925,1928,1931,1933],{"class":90,"line":1912},33,[88,1914,1915],{"class":244},"          if",[88,1917,1918],{"class":94}," (Object.",[88,1920,1921],{"class":251},"keys",[88,1923,1924],{"class":94},"(msg).",[88,1926,1927],{"class":432},"length",[88,1929,1930],{"class":244}," ===",[88,1932,1693],{"class":432},[88,1934,1935],{"class":94},") {\n",[88,1937,1939],{"class":90,"line":1938},34,[88,1940,1941],{"class":1516},"            \u002F\u002F handshake ack, trigger the demo stream targeted at this connection\n",[88,1943,1945,1948,1950,1953],{"class":90,"line":1944},35,[88,1946,1947],{"class":94},"            handshakeOk ",[88,1949,1648],{"class":244},[88,1951,1952],{"class":432}," true",[88,1954,1387],{"class":94},[88,1956,1958,1961,1964,1966,1968,1970,1973,1975,1978,1980,1982],{"class":90,"line":1957},36,[88,1959,1960],{"class":94},"            http.",[88,1962,1963],{"class":251},"get",[88,1965,255],{"class":94},[88,1967,1581],{"class":258},[88,1969,1436],{"class":432},[88,1971,1972],{"class":258},"}\u002Fdemo?duration=${",[88,1974,1462],{"class":432},[88,1976,1977],{"class":258},"}&connectionId=${",[88,1979,1282],{"class":94},[88,1981,1743],{"class":258},[88,1983,1470],{"class":94},[88,1985,1987,1990,1993,1996,1999,2001,2004],{"class":90,"line":1986},37,[88,1988,1989],{"class":94},"          } ",[88,1991,1992],{"class":244},"else",[88,1994,1995],{"class":244}," if",[88,1997,1998],{"class":94}," (msg.type ",[88,2000,1617],{"class":244},[88,2002,2003],{"class":432}," 1",[88,2005,1935],{"class":94},[88,2007,2009,2012,2015],{"class":90,"line":2008},38,[88,2010,2011],{"class":94},"            messagesReceived",[88,2013,2014],{"class":244},"++",[88,2016,1387],{"class":94},[88,2018,2020],{"class":90,"line":2019},39,[88,2021,2022],{"class":94},"          }\n",[88,2024,2026],{"class":90,"line":2025},40,[88,2027,2028],{"class":94},"        }\n",[88,2030,2032],{"class":90,"line":2031},41,[88,2033,1816],{"class":94},[88,2035,2037],{"class":90,"line":2036},42,[88,2038,347],{"emptyLinePlaceholder":346},[88,2040,2042,2044,2047,2050,2052,2055,2058,2061,2064],{"class":90,"line":2041},43,[88,2043,1774],{"class":94},[88,2045,2046],{"class":251},"setTimeout",[88,2048,2049],{"class":94},"(() ",[88,2051,1557],{"class":244},[88,2053,2054],{"class":94}," socket.",[88,2056,2057],{"class":251},"close",[88,2059,2060],{"class":94},"(), ",[88,2062,2063],{"class":432},"2000",[88,2065,1470],{"class":94},[88,2067,2069],{"class":90,"line":2068},44,[88,2070,2071],{"class":94},"    }\n",[88,2073,2075],{"class":90,"line":2074},45,[88,2076,2077],{"class":94},"  );\n",[88,2079,2081],{"class":90,"line":2080},46,[88,2082,347],{"emptyLinePlaceholder":346},[88,2084,2086,2088,2091,2094,2096,2098,2100,2102,2105,2108,2110,2112,2115],{"class":90,"line":2085},47,[88,2087,1593],{"class":251},[88,2089,2090],{"class":94},"(wsRes, { ",[88,2092,2093],{"class":258},"'ws: upgraded (101)'",[88,2095,1602],{"class":94},[88,2097,1606],{"class":1605},[88,2099,1609],{"class":94},[88,2101,1557],{"class":244},[88,2103,2104],{"class":94}," r ",[88,2106,2107],{"class":244},"&&",[88,2109,1614],{"class":94},[88,2111,1617],{"class":244},[88,2113,2114],{"class":432}," 101",[88,2116,1623],{"class":94},[88,2118,2120,2122],{"class":90,"line":2119},48,[88,2121,1593],{"class":251},[88,2123,2124],{"class":94},"({ handshakeOk, messagesReceived }, {\n",[88,2126,2128,2131,2133,2135,2137,2139],{"class":90,"line":2127},49,[88,2129,2130],{"class":258},"    'ws: handshake completed'",[88,2132,1602],{"class":94},[88,2134,1606],{"class":1605},[88,2136,1609],{"class":94},[88,2138,1557],{"class":244},[88,2140,2141],{"class":94}," r.handshakeOk,\n",[88,2143,2145,2148,2150,2152,2154,2156,2159,2162,2164],{"class":90,"line":2144},50,[88,2146,2147],{"class":258},"    'ws: received streamed messages'",[88,2149,1602],{"class":94},[88,2151,1606],{"class":1605},[88,2153,1609],{"class":94},[88,2155,1557],{"class":244},[88,2157,2158],{"class":94}," r.messagesReceived ",[88,2160,2161],{"class":244},">",[88,2163,1693],{"class":432},[88,2165,442],{"class":94},[88,2167,2169],{"class":90,"line":2168},51,[88,2170,2171],{"class":94},"  });\n",[88,2173,2175],{"class":90,"line":2174},52,[88,2176,347],{"emptyLinePlaceholder":346},[88,2178,2180,2183,2185,2187],{"class":90,"line":2179},53,[88,2181,2182],{"class":251},"  sleep",[88,2184,255],{"class":94},[88,2186,497],{"class":432},[88,2188,1470],{"class":94},[88,2190,2192],{"class":90,"line":2191},54,[88,2193,457],{"class":94},[10,2195,2196],{},[14,2197,2200],{"href":2198,"rel":2199},"https:\u002F\u002Fgithub.com\u002FNelsonBN\u002Fdemo-signalr-without-native-client\u002Fblob\u002Fmain\u002Ftests\u002FTest.SignalR.js",[18],"See the full script on GitHub",[22,2202,2204],{"id":2203},"conclusion","Conclusion",[10,2206,2207],{},"SignalR greatly simplifies building real-time applications in .NET, but that simplicity hides a fairly sophisticated protocol.",[10,2209,2210],{},"While a WebSocket only carries bytes, SignalR adds a full communication layer, including negotiation, handshake, method invocation, pings, streaming, and connection lifecycle management.",[10,2212,2213],{},"That abstraction is exactly what makes development more productive and testing more challenging.",[10,2215,2216],{},"Understanding what really happens \"on the wire\" helps not only to diagnose problems but also to build test tools that are more reliable and closer to reality.",[22,2218,2220],{"id":2219},"references","References",[56,2222,2223,2228,2235,2242,2249],{},[59,2224,2225],{},[14,2226,19],{"href":16,"rel":2227},[18],[59,2229,2230],{},[14,2231,2234],{"href":2232,"rel":2233},"https:\u002F\u002Fgithub.com\u002Fdotnet\u002Faspnetcore\u002Fblob\u002Fmain\u002Fsrc\u002FSignalR\u002Fdocs\u002Fspecs\u002FHubProtocol.md",[18],"SignalR Hub Protocol",[59,2236,2237],{},[14,2238,2241],{"href":2239,"rel":2240},"https:\u002F\u002Fgithub.com\u002Fdotnet\u002Faspnetcore\u002Fblob\u002Fmain\u002Fsrc\u002FSignalR\u002Fdocs\u002Fspecs\u002FTransportProtocols.md",[18],"Transport Protocols",[59,2243,2244],{},[14,2245,2248],{"href":2246,"rel":2247},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fjavascript\u002Fapi\u002F@microsoft\u002Fsignalr\u002Fmessagetype?view=signalr-js-latest",[18],"MessageType enum",[59,2250,2251],{},[14,2252,2255],{"href":2253,"rel":2254},"https:\u002F\u002Fgithub.com\u002FNelsonBN\u002Fdemo-signalr-without-native-client",[18],"Demo SignalR without native client",[2257,2258,2259],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sns5M, html code.shiki .sns5M{--shiki-default:#DBEDFF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":51,"searchDepth":98,"depth":98,"links":2261},[2262,2267,2271,2274,2275,2279,2280,2284,2285],{"id":24,"depth":98,"text":25,"children":2263},[2264,2265,2266],{"id":29,"depth":331,"text":30},{"id":104,"depth":331,"text":105},{"id":122,"depth":331,"text":123},{"id":228,"depth":98,"text":229,"children":2268},[2269,2270],{"id":268,"depth":331,"text":269},{"id":408,"depth":331,"text":409},{"id":680,"depth":98,"text":677,"children":2272},[2273],{"id":686,"depth":331,"text":687},{"id":718,"depth":98,"text":719},{"id":738,"depth":98,"text":739,"children":2276},[2277,2278],{"id":842,"depth":331,"text":843},{"id":972,"depth":331,"text":973},{"id":1010,"depth":98,"text":1011},{"id":1055,"depth":98,"text":1056,"children":2281},[2282,2283],{"id":1062,"depth":331,"text":1063},{"id":1346,"depth":331,"text":1347},{"id":2203,"depth":98,"text":2204},{"id":2219,"depth":98,"text":2220},"2026-08-02","SignalR is more than WebSockets. Learn the negotiate\u002Fhandshake flow, the record separator, and how to test SignalR without a native client","md",null,"en",{},"\u002Fblog\u002Fsignalr-trap-not-just-websockets",{"title":5,"description":2287},"blog\u002Fsignalr-trap-not-just-websockets",[2296,2297,2298,2299],".NET","C#","Tests","Communications","5FhRC_43Ienm9iqMPdz93xOtT0B0gbff5R4S3VWfZyo",1785635156445]