async Task ReceiveLoop() { ArraySegment <byte> buffer = new ArraySegment <byte>(new byte[RECEIVE_BUFFER_SIZE]); using (MemoryStream ms = new MemoryStream()) { WebSocketReceiveResult result = null; bool isClosing = false; try { while (socket.State == WebSocketState.Open) { // Reset memory stream for next message ms.Position = 0; ms.SetLength(0); // Continue receiving data until a full message is read or the socket is no longer open. do { try { // This call only throws WebSocketExceptions. result = await socket.ReceiveAsync(buffer, abortCancellationSource.Token).ConfigureAwait(false); } catch (OperationCanceledException) { log.LogVerbose($"[ReceiveLoop] Socket aborted while receiving."); break; } catch (WebSocketException wsex) { // Only two errors here should be InvalidState (if socket is aborted), // or ConnectionClosedPrematurely (with an inner exception detailing what happened). if (wsex.WebSocketErrorCode == WebSocketError.InvalidState) { log.LogVerbose($"[ReceiveLoop] Socket was aborted while receiving message. code = {wsex.WebSocketErrorCode}"); } else if (wsex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { log.LogError($"[ReceiveLoop] Socket closed prematurely while receiving: code = {wsex.WebSocketErrorCode}"); // Notify inherting object OnClosedPrematurely(); } else { log.LogError("[ReceiveLoop] Socket encountered error while receiving: " + $"code = {wsex.WebSocketErrorCode}, error = {wsex}"); // Notify inherting object OnClosedPrematurely(); } break; } if (result.MessageType == WebSocketMessageType.Close) { // Server is disconnecting us isClosing = true; log.LogVerbose($"[ReceiveLoop] Received close: {result.CloseStatusDescription} " + $"{result.CloseStatus} ({(int)result.CloseStatus})"); if (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseReceived || socket.State == WebSocketState.CloseSent) { try { log.LogVerbose("[ReceiveLoop] Completing close handshake with status NormalClosure (1000)..."); // Complete the closing handshake await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", abortCancellationSource.Token) .ConfigureAwait(false); log.LogVerbose("[ReceiveLoop] Completed close handshake."); } catch (OperationCanceledException) { log.LogVerbose($"[ReceiveLoop] Socket aborted while closing."); } catch (Exception ex) { log.LogError($"[ReceiveLoop] Failed to complete closing handshake: {ex}"); } } else { log.LogVerbose("[ReceiveLoop] Close handshake completed by remote end."); } // Notify inheriting object OnCloseReceived(result.CloseStatus.Value, result.CloseStatusDescription); break; } else { // Data message, append buffer to memory stream ms.Write(buffer.Array, 0, result.Count); } }while (socket.State == WebSocketState.Open && !result.EndOfMessage); if (isClosing || socket.State == WebSocketState.Aborted) { break; } // Parse the message string message = null; try { message = await ParseMessage(result.MessageType, ms).ConfigureAwait(false); } catch (Exception ex) { log.LogError($"[ReceiveLoop] Failed to parse message: {ex}"); } if (message != null) { if (DiscordApiData.TryParseJson(message, out DiscordApiData data)) { try { // Notify inheriting object that a payload has been received. await OnPayloadReceived(data).ConfigureAwait(false); } // Payload handlers can send other payloads which can result in two // valid exceptions that we do not want to bubble up. catch (InvalidOperationException) { // Socket was closed between receiving a payload and handling it log.LogVerbose("Received InvalidOperationException from OnPayloadReceived, " + "stopping receive loop..."); break; } catch (DiscordWebSocketException dwex) { if (dwex.Error == DiscordWebSocketError.ConnectionClosed) { // Socket was closed while a payload handler was sending another payload break; } else { // Unexpected error occured, we should only let it bubble up if the socket // is not open after the exception. if (socket.State == WebSocketState.Open) { log.LogError("[ReceiveLoop] Unexpected error from OnPayloadReceived: " + $"code = {dwex.Error}, error = {dwex}"); } else { // Don't log since that will be handled below throw; } } } } else { log.LogError($"[ReceiveLoop] Failed to parse JSON: \"{message}\""); } } } } catch (Exception ex) { log.LogError($"[ReceiveLoop] Uncaught exception: {ex}"); OnClosedPrematurely(); } } }
/// <exception cref="DiscordHttpApiException"></exception> async Task <DiscordApiData> ParseResponse(HttpResponseMessage response, RateLimitHeaders rateLimitHeaders) { // Read response payload as string string json; if (response.StatusCode == HttpStatusCode.NoContent) { json = null; } else { json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); } // Attempt to parse the payload as JSON. DiscordApiData data; if (DiscordApiData.TryParseJson(json, out data)) { if (response.IsSuccessStatusCode) { // If successful, no more action is required. return(data); } else { string message = null; DiscordHttpErrorCode errorCode = DiscordHttpErrorCode.None; // Get the Discord-specific error code if it exists. if ((int)response.StatusCode == 429) { errorCode = DiscordHttpErrorCode.TooManyRequests; } else if (data.ContainsKey("code")) { long?code = data.GetInt64("code"); if (code.HasValue) { errorCode = (DiscordHttpErrorCode)code; } } // Get the message. if (data.ContainsKey("content")) { IList <DiscordApiData> content = data.GetArray("content"); StringBuilder sb = new StringBuilder(); for (int i = 0; i < content.Count; i++) { sb.Append(content[i]); if (i < content.Count - 1) { sb.Append(", "); } } message = sb.ToString(); } else if (data.ContainsKey("message")) { message = data.GetString("message"); } else if (response.StatusCode == HttpStatusCode.BadRequest && data.Type == DiscordApiDataType.Container) { StringBuilder sb = new StringBuilder(); foreach (KeyValuePair <string, DiscordApiData> pair in data.Entries) { sb.Append($"{pair.Key}: "); if (pair.Value.Type != DiscordApiDataType.Array) { // Shouldn't happen, but if it does then this error is not one we can parse. // Set sb to null so that message ends up null and let the application get // the raw JSON payload so they at least know what happened until we can // implement the correct parser. sb = null; break; } bool addComma = false; foreach (DiscordApiData errorData in pair.Value.Values) { if (addComma) { sb.Append(", "); } sb.Append(errorData.ToString()); addComma = true; } sb.AppendLine(); } message = sb?.ToString(); } // Throw the appropriate exception if (message != null) // If null, let the "unknown error" exception be thrown { if ((int)response.StatusCode == 429 && rateLimitHeaders != null) { throw new DiscordHttpRateLimitException(rateLimitHeaders, message, errorCode, response.StatusCode); } else { throw new DiscordHttpApiException(message, errorCode, response.StatusCode); } } } } throw new DiscordHttpApiException($"Unknown error. Response: {json}", DiscordHttpErrorCode.None, response.StatusCode); }