diff --git a/src/Discord.Net.Core/API/DiscordRestApiClient.cs b/src/Discord.Net.Core/API/DiscordRestApiClient.cs
index 0d0b7914b..61e56ca13 100644
--- a/src/Discord.Net.Core/API/DiscordRestApiClient.cs
+++ b/src/Discord.Net.Core/API/DiscordRestApiClient.cs
@@ -32,26 +32,29 @@ namespace Discord.API
protected readonly JsonSerializer _serializer;
protected readonly SemaphoreSlim _stateLock;
private readonly RestClientProvider _restClientProvider;
- private readonly string _userAgent;
protected string _authToken;
protected bool _isDisposed;
private CancellationTokenSource _loginCancelToken;
private IRestClient _restClient;
+ private bool _fetchCurrentUser;
+
+ public RetryMode DefaultRetryMode { get; }
+ public string UserAgent { get; }
public LoginState LoginState { get; private set; }
public TokenType AuthTokenType { get; private set; }
public User CurrentUser { get; private set; }
public RequestQueue RequestQueue { get; private set; }
- internal bool FetchCurrentUser { get; set; }
- public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, JsonSerializer serializer = null, RequestQueue requestQueue = null)
+ public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry,
+ JsonSerializer serializer = null, RequestQueue requestQueue = null, bool fetchCurrentUser = true)
{
_restClientProvider = restClientProvider;
- _userAgent = userAgent;
+ UserAgent = userAgent;
_serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() };
RequestQueue = requestQueue;
- FetchCurrentUser = true;
+ _fetchCurrentUser = fetchCurrentUser;
_stateLock = new SemaphoreSlim(1, 1);
@@ -61,7 +64,7 @@ namespace Discord.API
{
_restClient = _restClientProvider(baseUrl);
_restClient.SetHeader("accept", "*/*");
- _restClient.SetHeader("user-agent", _userAgent);
+ _restClient.SetHeader("user-agent", UserAgent);
_restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken));
}
internal static string GetPrefixedToken(TokenType tokenType, string token)
@@ -120,8 +123,8 @@ namespace Discord.API
_authToken = token;
_restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken));
- if (FetchCurrentUser)
- CurrentUser = await GetMyUserAsync(new RequestOptions { IgnoreState = true }).ConfigureAwait(false);
+ if (_fetchCurrentUser)
+ CurrentUser = await GetMyUserAsync(new RequestOptions { IgnoreState = true, RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false);
LoginState = LoginState.LoggedIn;
}
@@ -257,6 +260,8 @@ namespace Discord.API
{
if (!request.Options.IgnoreState)
CheckState();
+ if (request.Options.RetryMode == null)
+ request.Options.RetryMode = DefaultRetryMode;
var stopwatch = Stopwatch.StartNew();
var responseStream = await RequestQueue.SendAsync(request).ConfigureAwait(false);
diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs
index b35f0d745..bb7077472 100644
--- a/src/Discord.Net.Core/DiscordConfig.cs
+++ b/src/Discord.Net.Core/DiscordConfig.cs
@@ -19,6 +19,9 @@ namespace Discord
public const int MaxMessagesPerBatch = 100;
public const int MaxUsersPerBatch = 1000;
+ /// Gets or sets how a request should act in the case of an error, by default.
+ public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry;
+
/// Gets or sets the minimum log level severity that will be sent to the LogMessage event.
public LogSeverity LogLevel { get; set; } = LogSeverity.Info;
}
diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs
index b48c76a37..ab447e520 100644
--- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs
+++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs
@@ -12,6 +12,7 @@ namespace Discord
DateTimeOffset? JoinedAt { get; }
/// Gets the nickname for this user.
string Nickname { get; }
+ /// Gets the guild-level permissions for this user.
GuildPermissions GuildPermissions { get; }
/// Gets the guild for this user.
diff --git a/src/Discord.Net.Core/Net/Queue/RequestQueue.cs b/src/Discord.Net.Core/Net/Queue/RequestQueue.cs
index 1ea586481..52ad90f11 100644
--- a/src/Discord.Net.Core/Net/Queue/RequestQueue.cs
+++ b/src/Discord.Net.Core/Net/Queue/RequestQueue.cs
@@ -1,6 +1,8 @@
using System;
using System.Collections.Concurrent;
+#if DEBUG_LIMITS
using System.Diagnostics;
+#endif
using System.IO;
using System.Linq;
using System.Threading;
@@ -63,7 +65,11 @@ namespace Discord.Net.Queue
public async Task SendAsync(RestRequest request)
{
- request.CancelToken = _requestCancelToken;
+ if (request.Options.CancelToken.CanBeCanceled)
+ request.Options.CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_requestCancelToken, request.Options.CancelToken).Token;
+ else
+ request.Options.CancelToken = _requestCancelToken;
+
var bucket = GetOrCreateBucket(request.Options.BucketId, request);
return await bucket.SendAsync(request).ConfigureAwait(false);
}
diff --git a/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs
index 8ee52171c..332177de8 100644
--- a/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs
+++ b/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs
@@ -1,5 +1,4 @@
-using Discord.Net.Rest;
-using Newtonsoft.Json;
+using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
#if DEBUG_LIMITS
@@ -88,7 +87,10 @@ namespace Discord.Net.Queue
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] (!) 502");
#endif
- continue; //Continue
+ if ((request.Options.RetryMode & RetryMode.Retry502) == 0)
+ throw new HttpException(HttpStatusCode.BadGateway, null);
+
+ continue; //Retry
default:
string reason = null;
if (response.Stream != null)
@@ -115,13 +117,28 @@ namespace Discord.Net.Queue
return response.Stream;
}
}
+ catch (TimeoutException)
+ {
#if DEBUG_LIMITS
- catch
+ Debug.WriteLine($"[{id}] Timeout");
+#endif
+ if ((request.Options.RetryMode & RetryMode.RetryTimeouts) == 0)
+ throw;
+
+ await Task.Delay(500);
+ continue; //Retry
+ }
+ catch (Exception)
{
+#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Error");
- throw;
- }
#endif
+ if ((request.Options.RetryMode & RetryMode.RetryErrors) == 0)
+ throw;
+
+ await Task.Delay(500);
+ continue; //Retry
+ }
finally
{
UpdateRateLimit(id, request, info, lag, false);
@@ -140,7 +157,7 @@ namespace Discord.Net.Queue
while (true)
{
- if (DateTimeOffset.UtcNow > request.TimeoutAt || request.CancelToken.IsCancellationRequested)
+ if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested)
{
if (!isRateLimited)
throw new TimeoutException();
@@ -162,6 +179,10 @@ namespace Discord.Net.Queue
isRateLimited = true;
await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false);
}
+
+ if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0)
+ throw new RateLimitedException();
+
if (resetAt.HasValue)
{
if (resetAt > timeoutAt)
@@ -171,7 +192,7 @@ namespace Discord.Net.Queue
Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)");
#endif
if (millis > 0)
- await Task.Delay(millis, request.CancelToken).ConfigureAwait(false);
+ await Task.Delay(millis, request.Options.CancelToken).ConfigureAwait(false);
}
else
{
@@ -180,7 +201,7 @@ namespace Discord.Net.Queue
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)");
#endif
- await Task.Delay(500, request.CancelToken).ConfigureAwait(false);
+ await Task.Delay(500, request.Options.CancelToken).ConfigureAwait(false);
}
continue;
}
diff --git a/src/Discord.Net.Core/Net/Queue/Requests/JsonRestRequest.cs b/src/Discord.Net.Core/Net/Queue/Requests/JsonRestRequest.cs
index 75869d52a..83c5e0eb5 100644
--- a/src/Discord.Net.Core/Net/Queue/Requests/JsonRestRequest.cs
+++ b/src/Discord.Net.Core/Net/Queue/Requests/JsonRestRequest.cs
@@ -15,7 +15,7 @@ namespace Discord.Net.Queue
public override async Task SendAsync()
{
- return await Client.SendAsync(Method, Endpoint, Json, CancelToken, Options.HeaderOnly).ConfigureAwait(false);
+ return await Client.SendAsync(Method, Endpoint, Json, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false);
}
}
}
diff --git a/src/Discord.Net.Core/Net/Queue/Requests/MultipartRestRequest.cs b/src/Discord.Net.Core/Net/Queue/Requests/MultipartRestRequest.cs
index d132ef395..424a5325e 100644
--- a/src/Discord.Net.Core/Net/Queue/Requests/MultipartRestRequest.cs
+++ b/src/Discord.Net.Core/Net/Queue/Requests/MultipartRestRequest.cs
@@ -16,7 +16,7 @@ namespace Discord.Net.Queue
public override async Task SendAsync()
{
- return await Client.SendAsync(Method, Endpoint, MultipartParams, CancelToken, Options.HeaderOnly).ConfigureAwait(false);
+ return await Client.SendAsync(Method, Endpoint, MultipartParams, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false);
}
}
}
diff --git a/src/Discord.Net.Core/Net/Queue/Requests/RestRequest.cs b/src/Discord.Net.Core/Net/Queue/Requests/RestRequest.cs
index 5d5bc1e59..7f358e786 100644
--- a/src/Discord.Net.Core/Net/Queue/Requests/RestRequest.cs
+++ b/src/Discord.Net.Core/Net/Queue/Requests/RestRequest.cs
@@ -1,7 +1,6 @@
using Discord.Net.Rest;
using System;
using System.IO;
-using System.Threading;
using System.Threading.Tasks;
namespace Discord.Net.Queue
@@ -14,7 +13,6 @@ namespace Discord.Net.Queue
public DateTimeOffset? TimeoutAt { get; }
public TaskCompletionSource Promise { get; }
public RequestOptions Options { get; }
- public CancellationToken CancelToken { get; internal set; }
public RestRequest(IRestClient client, string method, string endpoint, RequestOptions options)
{
@@ -24,14 +22,13 @@ namespace Discord.Net.Queue
Method = method;
Endpoint = endpoint;
Options = options;
- CancelToken = CancellationToken.None;
TimeoutAt = options.Timeout.HasValue ? DateTimeOffset.UtcNow.AddMilliseconds(options.Timeout.Value) : (DateTimeOffset?)null;
Promise = new TaskCompletionSource();
}
public virtual async Task SendAsync()
{
- return await Client.SendAsync(Method, Endpoint, CancelToken, Options.HeaderOnly).ConfigureAwait(false);
+ return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false);
}
}
}
diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs
index b82ec29c8..4f5910c53 100644
--- a/src/Discord.Net.Core/RequestOptions.cs
+++ b/src/Discord.Net.Core/RequestOptions.cs
@@ -1,11 +1,18 @@
-namespace Discord
+using System.Threading;
+
+namespace Discord
{
public class RequestOptions
{
public static RequestOptions Default => new RequestOptions();
- /// The max time, in milliseconds, to wait for this request to complete. If null, a request will not time out. If a rate limit has been triggered for this request's bucket and will not be unpaused in time, this request will fail immediately.
+ ///
+ /// The max time, in milliseconds, to wait for this request to complete. If null, a request will not time out.
+ /// If a rate limit has been triggered for this request's bucket and will not be unpaused in time, this request will fail immediately.
+ ///
public int? Timeout { get; set; }
+ public CancellationToken CancelToken { get; set; } = CancellationToken.None;
+ public RetryMode? RetryMode { get; set; }
public bool HeaderOnly { get; internal set; }
internal bool IgnoreState { get; set; }
@@ -13,7 +20,7 @@
internal bool IsClientBucket { get; set; }
internal static RequestOptions CreateOrClone(RequestOptions options)
- {
+ {
if (options == null)
return new RequestOptions();
else
diff --git a/src/Discord.Net.Core/RetryMode.cs b/src/Discord.Net.Core/RetryMode.cs
new file mode 100644
index 000000000..9dccfc313
--- /dev/null
+++ b/src/Discord.Net.Core/RetryMode.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace Discord
+{
+ /// Specifies how a request should act in the case of an error.
+ [Flags]
+ public enum RetryMode
+ {
+ /// If a request fails, an exception is thrown immediately.
+ AlwaysFail = 0x0,
+ /// Retry if a request timed out.
+ RetryTimeouts = 0x1,
+ /// Retry if a request failed due to a network error.
+ RetryErrors = 0x2,
+ /// Retry if a request failed due to a ratelimit.
+ RetryRatelimit = 0x4,
+ /// Retry if a request failed due to an HTTP error 502.
+ Retry502 = 0x8,
+ /// Continuously retry a request until it times out, its cancel token is triggered, or the server responds with a non-502 error.
+ AlwaysRetry = RetryTimeouts | RetryErrors | RetryRatelimit | Retry502,
+ }
+}
diff --git a/src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs b/src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs
index 720c975c0..ee6ad84e1 100644
--- a/src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs
+++ b/src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs
@@ -69,15 +69,13 @@ namespace Discord.API
public ConnectionState ConnectionState { get; private set; }
public DiscordRpcApiClient(string clientId, string userAgent, string origin, RestClientProvider restClientProvider, WebSocketProvider webSocketProvider,
- JsonSerializer serializer = null, RequestQueue requestQueue = null)
- : base(restClientProvider, userAgent, serializer, requestQueue)
+ RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null, RequestQueue requestQueue = null)
+ : base(restClientProvider, userAgent, defaultRetryMode, serializer, requestQueue, false)
{
_connectionLock = new SemaphoreSlim(1, 1);
_clientId = clientId;
_origin = origin;
- FetchCurrentUser = false;
-
_requestQueue = requestQueue ?? new RequestQueue();
_requests = new ConcurrentDictionary();
diff --git a/src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs
index f0dd5f852..9592e2a04 100644
--- a/src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs
+++ b/src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs
@@ -32,11 +32,12 @@ namespace Discord.API
public ConnectionState ConnectionState { get; private set; }
- public DiscordSocketApiClient(RestClientProvider restClientProvider, string userAgent, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null)
- : base(restClientProvider, userAgent, serializer, requestQueue)
+ public DiscordSocketApiClient(RestClientProvider restClientProvider, string userAgent, WebSocketProvider webSocketProvider,
+ RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null, RequestQueue requestQueue = null)
+ : base(restClientProvider, userAgent, defaultRetryMode, serializer, requestQueue, true)
{
_gatewayClient = webSocketProvider();
- //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+)
+ //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .NET Framework 4.6+)
_gatewayClient.BinaryMessage += async (data, index, count) =>
{
using (var compressed = new MemoryStream(data, index + 2, count - 2))
diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs
index a150a6d15..2f952bdaa 100644
--- a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs
+++ b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs
@@ -221,7 +221,5 @@ namespace Discord.WebSocket
remove { _recipientRemovedEvent.Remove(value); }
}
private readonly AsyncEvent> _recipientRemovedEvent = new AsyncEvent>();
-
- //TODO: Add PresenceUpdated? VoiceStateUpdated?, VoiceConnected, VoiceDisconnected;
}
}
diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs
index fde2d100b..0faa3cbbd 100644
--- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs
+++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs
@@ -130,7 +130,7 @@ namespace Discord.WebSocket
protected override async Task OnLoginAsync(TokenType tokenType, string token)
{
- var voiceRegions = await ApiClient.GetVoiceRegionsAsync(new RequestOptions { IgnoreState = true}).ConfigureAwait(false);
+ var voiceRegions = await ApiClient.GetVoiceRegionsAsync(new RequestOptions { IgnoreState = true, RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false);
_voiceRegions = voiceRegions.Select(x => RestVoiceRegion.Create(this, x)).ToImmutableDictionary(x => x.Id);
}
protected override async Task OnLogoutAsync()