Skip to content

Commit

Permalink
Pulling retry-on-rate-limit logic up to SlackApiClient to avoid reusi…
Browse files Browse the repository at this point in the history
…ng requests
  • Loading branch information
Simon Oxtoby committed Aug 13, 2020
1 parent 1cdaedf commit aba80d2
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 40 deletions.
34 changes: 16 additions & 18 deletions SlackNet/Http.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,34 @@ class Http : IHttp
{
private readonly HttpClient _client;
private readonly SlackJsonSettings _jsonSettings;
private readonly bool _retryOnRateLimit;

public Http(HttpClient client, SlackJsonSettings jsonSettings, bool retryOnRateLimit = true)
public Http(HttpClient client, SlackJsonSettings jsonSettings)
{
_client = client;
_jsonSettings = jsonSettings;
_retryOnRateLimit = retryOnRateLimit;
}

public async Task<T> Execute<T>(HttpRequestMessage requestMessage, CancellationToken? cancellationToken = null)
{
while (true)
{
var response = await _client.SendAsync(requestMessage, cancellationToken ?? CancellationToken.None).ConfigureAwait(false);
var response = await _client.SendAsync(requestMessage, cancellationToken ?? CancellationToken.None).ConfigureAwait(false);

if (_retryOnRateLimit && (int)response.StatusCode == 429) // TODO use the enum when TooManyRequests becomes available
{
await Task.Delay(response.Headers.RetryAfter.Delta ?? TimeSpan.FromSeconds(1)).ConfigureAwait(false);
}
else
{
response.EnsureSuccessStatusCode();
return response.Content.Headers.ContentType.MediaType == "application/json"
? await Deserialize<T>(response).ConfigureAwait(false)
: default;
}
}
if ((int)response.StatusCode == 429) // TODO use the enum when TooManyRequests becomes available
throw new SlackRateLimitException(response.Headers.RetryAfter.Delta);
response.EnsureSuccessStatusCode();

return response.Content.Headers.ContentType.MediaType == "application/json"
? await Deserialize<T>(response).ConfigureAwait(false)
: default;
}

private async Task<T> Deserialize<T>(HttpResponseMessage response) =>
JsonSerializer.Create(_jsonSettings.SerializerSettings).Deserialize<T>(new JsonTextReader(new StreamReader(await response.Content.ReadAsStreamAsync().ConfigureAwait(false))));
}

class SlackRateLimitException : Exception
{
public TimeSpan? RetryAfter { get; }

public SlackRateLimitException(TimeSpan? retryAfter) => RetryAfter = retryAfter;
}
}
55 changes: 33 additions & 22 deletions SlackNet/SlackApiClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
Expand Down Expand Up @@ -120,6 +121,7 @@ public class SlackApiClient : ISlackApiClient
private readonly ISlackUrlBuilder _urlBuilder;
private readonly string _token;
private readonly SlackJsonSettings _jsonSettings;
public bool DisableRetryOnRateLimit { get; set; }

public SlackApiClient(string token)
{
Expand Down Expand Up @@ -192,12 +194,9 @@ public Task Get(string apiMethod, Args args, CancellationToken? cancellationToke
/// <param name="apiMethod">Name of Slack method.</param>
/// <param name="args">Arguments to send to Slack. The "token" parameter will be filled in automatically.</param>
/// <param name="cancellationToken"></param>
public async Task<T> Get<T>(string apiMethod, Args args, CancellationToken? cancellationToken) where T : class
{
var requestMessage = new HttpRequestMessage(HttpMethod.Get, Url(apiMethod, args));
return Deserialize<T>(await _http.Execute<WebApiResponse>(requestMessage, cancellationToken ?? CancellationToken.None).ConfigureAwait(false));
}

public Task<T> Get<T>(string apiMethod, Args args, CancellationToken? cancellationToken) where T : class =>
WebApiRequest<T>(() => new HttpRequestMessage(HttpMethod.Get, Url(apiMethod, args)), cancellationToken);

/// <summary>
/// Calls a Slack API that requires POST content.
/// </summary>
Expand Down Expand Up @@ -235,11 +234,8 @@ public Task Post(string apiMethod, Args args, HttpContent content, CancellationT
/// <param name="args">Arguments to send to Slack. The "token" parameter will be filled in automatically.</param>
/// <param name="content">POST body content. Should be either <see cref="FormUrlEncodedContent"/> or <see cref="MultipartFormDataContent"/>.</param>
/// <param name="cancellationToken"></param>
public async Task<T> Post<T>(string apiMethod, Args args, HttpContent content, CancellationToken? cancellationToken) where T : class
{
var requestMessage = new HttpRequestMessage(HttpMethod.Post, Url(apiMethod, args)) { Content = content };
return Deserialize<T>(await _http.Execute<WebApiResponse>(requestMessage, cancellationToken ?? CancellationToken.None).ConfigureAwait(false));
}
public Task<T> Post<T>(string apiMethod, Args args, HttpContent content, CancellationToken? cancellationToken) where T : class =>
WebApiRequest<T>(() => new HttpRequestMessage(HttpMethod.Post, Url(apiMethod, args)) { Content = content }, cancellationToken);

/// <summary>
/// Posts a message to a response URL provided by e.g. <see cref="InteractionRequest"/> or <see cref="SlashCommand"/>.
Expand All @@ -250,16 +246,14 @@ public async Task<T> Post<T>(string apiMethod, Args args, HttpContent content, C
public Task Respond(string responseUrl, IReadOnlyMessage message, CancellationToken? cancellationToken) =>
Post<object>(responseUrl, message, cancellationToken);

private async Task<T> Post<T>(string requestUri, object body, CancellationToken? cancellationToken) where T : class
{
var requestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri);
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
requestMessage.Content = new StringContent(JsonConvert.SerializeObject(body, _jsonSettings.SerializerSettings), Encoding.UTF8, "application/json");

var response = await _http.Execute<WebApiResponse>(requestMessage, cancellationToken ?? CancellationToken.None).ConfigureAwait(false)
?? new WebApiResponse { Ok = true };
return Deserialize<T>(response);
}
private Task<T> Post<T>(string requestUri, object body, CancellationToken? cancellationToken) where T : class =>
WebApiRequest<T>(() =>
{
var requestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri);
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
requestMessage.Content = new StringContent(JsonConvert.SerializeObject(body, _jsonSettings.SerializerSettings), Encoding.UTF8, "application/json");
return requestMessage;
}, cancellationToken);

private string Url(string apiMethod) =>
_urlBuilder.Url(apiMethod, new Args());
Expand All @@ -271,6 +265,23 @@ private string Url(string apiMethod, Args args)
return _urlBuilder.Url(apiMethod, args);
}

private async Task<T> WebApiRequest<T>(Func<HttpRequestMessage> createRequest, CancellationToken? cancellationToken) where T : class
{
while (true)
{
try
{
var response = await _http.Execute<WebApiResponse>(createRequest(), cancellationToken ?? CancellationToken.None).ConfigureAwait(false)
?? new WebApiResponse { Ok = true };
return Deserialize<T>(response);
}
catch (SlackRateLimitException e) when (!DisableRetryOnRateLimit)
{
await Task.Delay(e.RetryAfter ?? TimeSpan.FromSeconds(1)).ConfigureAwait(false);
}
}
}

private T Deserialize<T>(WebApiResponse response) where T : class =>
response.Ok
? response.Data?.ToObject<T>(JsonSerializer.Create(_jsonSettings.SerializerSettings))
Expand Down

0 comments on commit aba80d2

Please sign in to comment.