WebSocketTransport.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. import { HeaderNames } from "./HeaderNames";
  4. import { LogLevel } from "./ILogger";
  5. import { TransferFormat } from "./ITransport";
  6. import { Arg, getDataDetail, getUserAgentHeader, Platform } from "./Utils";
  7. /** @private */
  8. export class WebSocketTransport {
  9. constructor(httpClient, accessTokenFactory, logger, logMessageContent, webSocketConstructor, headers) {
  10. this._logger = logger;
  11. this._accessTokenFactory = accessTokenFactory;
  12. this._logMessageContent = logMessageContent;
  13. this._webSocketConstructor = webSocketConstructor;
  14. this._httpClient = httpClient;
  15. this.onreceive = null;
  16. this.onclose = null;
  17. this._headers = headers;
  18. }
  19. async connect(url, transferFormat) {
  20. Arg.isRequired(url, "url");
  21. Arg.isRequired(transferFormat, "transferFormat");
  22. Arg.isIn(transferFormat, TransferFormat, "transferFormat");
  23. this._logger.log(LogLevel.Trace, "(WebSockets transport) Connecting.");
  24. let token;
  25. if (this._accessTokenFactory) {
  26. token = await this._accessTokenFactory();
  27. }
  28. return new Promise((resolve, reject) => {
  29. url = url.replace(/^http/, "ws");
  30. let webSocket;
  31. const cookies = this._httpClient.getCookieString(url);
  32. let opened = false;
  33. if (Platform.isNode || Platform.isReactNative) {
  34. const headers = {};
  35. const [name, value] = getUserAgentHeader();
  36. headers[name] = value;
  37. if (token) {
  38. headers[HeaderNames.Authorization] = `Bearer ${token}`;
  39. }
  40. if (cookies) {
  41. headers[HeaderNames.Cookie] = cookies;
  42. }
  43. // Only pass headers when in non-browser environments
  44. webSocket = new this._webSocketConstructor(url, undefined, {
  45. headers: { ...headers, ...this._headers },
  46. });
  47. }
  48. else {
  49. if (token) {
  50. url += (url.indexOf("?") < 0 ? "?" : "&") + `access_token=${encodeURIComponent(token)}`;
  51. }
  52. }
  53. if (!webSocket) {
  54. // Chrome is not happy with passing 'undefined' as protocol
  55. webSocket = new this._webSocketConstructor(url);
  56. }
  57. if (transferFormat === TransferFormat.Binary) {
  58. webSocket.binaryType = "arraybuffer";
  59. }
  60. webSocket.onopen = (_event) => {
  61. this._logger.log(LogLevel.Information, `WebSocket connected to ${url}.`);
  62. this._webSocket = webSocket;
  63. opened = true;
  64. resolve();
  65. };
  66. webSocket.onerror = (event) => {
  67. let error = null;
  68. // ErrorEvent is a browser only type we need to check if the type exists before using it
  69. if (typeof ErrorEvent !== "undefined" && event instanceof ErrorEvent) {
  70. error = event.error;
  71. }
  72. else {
  73. error = "There was an error with the transport";
  74. }
  75. this._logger.log(LogLevel.Information, `(WebSockets transport) ${error}.`);
  76. };
  77. webSocket.onmessage = (message) => {
  78. this._logger.log(LogLevel.Trace, `(WebSockets transport) data received. ${getDataDetail(message.data, this._logMessageContent)}.`);
  79. if (this.onreceive) {
  80. try {
  81. this.onreceive(message.data);
  82. }
  83. catch (error) {
  84. this._close(error);
  85. return;
  86. }
  87. }
  88. };
  89. webSocket.onclose = (event) => {
  90. // Don't call close handler if connection was never established
  91. // We'll reject the connect call instead
  92. if (opened) {
  93. this._close(event);
  94. }
  95. else {
  96. let error = null;
  97. // ErrorEvent is a browser only type we need to check if the type exists before using it
  98. if (typeof ErrorEvent !== "undefined" && event instanceof ErrorEvent) {
  99. error = event.error;
  100. }
  101. else {
  102. error = "WebSocket failed to connect. The connection could not be found on the server,"
  103. + " either the endpoint may not be a SignalR endpoint,"
  104. + " the connection ID is not present on the server, or there is a proxy blocking WebSockets."
  105. + " If you have multiple servers check that sticky sessions are enabled.";
  106. }
  107. reject(new Error(error));
  108. }
  109. };
  110. });
  111. }
  112. send(data) {
  113. if (this._webSocket && this._webSocket.readyState === this._webSocketConstructor.OPEN) {
  114. this._logger.log(LogLevel.Trace, `(WebSockets transport) sending data. ${getDataDetail(data, this._logMessageContent)}.`);
  115. this._webSocket.send(data);
  116. return Promise.resolve();
  117. }
  118. return Promise.reject("WebSocket is not in the OPEN state");
  119. }
  120. stop() {
  121. if (this._webSocket) {
  122. // Manually invoke onclose callback inline so we know the HttpConnection was closed properly before returning
  123. // This also solves an issue where websocket.onclose could take 18+ seconds to trigger during network disconnects
  124. this._close(undefined);
  125. }
  126. return Promise.resolve();
  127. }
  128. _close(event) {
  129. // webSocket will be null if the transport did not start successfully
  130. if (this._webSocket) {
  131. // Clear websocket handlers because we are considering the socket closed now
  132. this._webSocket.onclose = () => { };
  133. this._webSocket.onmessage = () => { };
  134. this._webSocket.onerror = () => { };
  135. this._webSocket.close();
  136. this._webSocket = undefined;
  137. }
  138. this._logger.log(LogLevel.Trace, "(WebSockets transport) socket closed.");
  139. if (this.onclose) {
  140. if (this._isCloseEvent(event) && (event.wasClean === false || event.code !== 1000)) {
  141. this.onclose(new Error(`WebSocket closed with status code: ${event.code} (${event.reason || "no reason given"}).`));
  142. }
  143. else if (event instanceof Error) {
  144. this.onclose(event);
  145. }
  146. else {
  147. this.onclose();
  148. }
  149. }
  150. }
  151. _isCloseEvent(event) {
  152. return event && typeof event.wasClean === "boolean" && typeof event.code === "number";
  153. }
  154. }
  155. //# sourceMappingURL=WebSocketTransport.js.map