From bcc2742e81bf3668a3a5fd1efffc5df1977a93ed Mon Sep 17 00:00:00 2001 From: Daan Boerlage Date: Sun, 14 Nov 2021 01:12:32 +0100 Subject: [PATCH] Add retry logic for Post, Patch and Delete to the HttpAbstractions --- src/Core/HttpAbstractions.cs | 135 ++++++++++++++++++++++++++--------- 1 file changed, 102 insertions(+), 33 deletions(-) diff --git a/src/Core/HttpAbstractions.cs b/src/Core/HttpAbstractions.cs index e20fe2d..629de74 100644 --- a/src/Core/HttpAbstractions.cs +++ b/src/Core/HttpAbstractions.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; @@ -24,24 +26,29 @@ namespace Geekbot.Core return client; } - public static async Task Get(Uri location, HttpClient httpClient = null, bool disposeClient = true) + public static async Task Get(Uri location, HttpClient httpClient = null, bool disposeClient = true, int maxRetries = 3) { httpClient ??= CreateDefaultClient(); httpClient.BaseAddress = location; - var response = await httpClient.GetAsync(location.PathAndQuery); - response.EnsureSuccessStatusCode(); - var stringResponse = await response.Content.ReadAsStringAsync(); - - if (disposeClient) + HttpResponseMessage response; + try { - httpClient.Dispose(); + response = await Execute(() => httpClient.GetAsync(location.PathAndQuery), maxRetries); + } + finally + { + if (disposeClient) + { + httpClient.Dispose(); + } } + var stringResponse = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize(stringResponse); } - public static async Task Post(Uri location, object data, HttpClient httpClient = null, bool disposeClient = true) + public static async Task Post(Uri location, object data, HttpClient httpClient = null, bool disposeClient = true, int maxRetries = 3) { httpClient ??= CreateDefaultClient(); httpClient.BaseAddress = location; @@ -51,69 +58,131 @@ namespace Geekbot.Core Encoding.UTF8, "application/json" ); - var response = await httpClient.PostAsync(location.PathAndQuery, content); - response.EnsureSuccessStatusCode(); - var stringResponse = await response.Content.ReadAsStringAsync(); - if (disposeClient) + HttpResponseMessage response; + try { - httpClient.Dispose(); + response = await Execute(() => httpClient.PostAsync(location, content), maxRetries); + } + finally + { + if (disposeClient) + { + httpClient.Dispose(); + } } + var stringResponse = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize(stringResponse); } - public static async Task Post(Uri location, object data, HttpClient httpClient = null, bool disposeClient = true) + public static async Task Post(Uri location, object data, HttpClient httpClient = null, bool disposeClient = true, int maxRetries = 3) { httpClient ??= CreateDefaultClient(); httpClient.BaseAddress = location; var content = new StringContent( - System.Text.Json.JsonSerializer.Serialize(data, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }), + JsonSerializer.Serialize(data, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }), Encoding.UTF8, "application/json" ); - var response = await httpClient.PostAsync(location, content); - response.EnsureSuccessStatusCode(); - - if (disposeClient) + try { - httpClient.Dispose(); + await Execute(() => httpClient.PostAsync(location, content), maxRetries); + } + finally + { + if (disposeClient) + { + httpClient.Dispose(); + } } } - public static async Task Patch(Uri location, object data, HttpClient httpClient = null, bool disposeClient = true) + public static async Task Patch(Uri location, object data, HttpClient httpClient = null, bool disposeClient = true, int maxRetries = 3) { httpClient ??= CreateDefaultClient(); httpClient.BaseAddress = location; var content = new StringContent( - System.Text.Json.JsonSerializer.Serialize(data, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }), + JsonSerializer.Serialize(data, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }), Encoding.UTF8, "application/json" ); - var response = await httpClient.PatchAsync(location, content); - response.EnsureSuccessStatusCode(); - - if (disposeClient) + try { - httpClient.Dispose(); + await Execute(() => httpClient.PatchAsync(location, content), maxRetries); + } + finally + { + if (disposeClient) + { + httpClient.Dispose(); + } } } - public static async Task Delete(Uri location, HttpClient httpClient = null, bool disposeClient = true) + public static async Task Delete(Uri location, HttpClient httpClient = null, bool disposeClient = true, int maxRetries = 3) { httpClient ??= CreateDefaultClient(); httpClient.BaseAddress = location; - var response = await httpClient.DeleteAsync(location); - response.EnsureSuccessStatusCode(); - - if (disposeClient) + try { - httpClient.Dispose(); + await Execute(() => httpClient.DeleteAsync(location), maxRetries); + } + finally + { + if (disposeClient) + { + httpClient.Dispose(); + } + } + } + + private static async Task Execute(Func> request, int maxRetries) + { + var attempt = 0; + while (true) + { + var response = await request(); + if (!response.IsSuccessStatusCode) + { + if (attempt >= maxRetries) + { + throw new HttpRequestException($"Request failed after {attempt} attempts"); + } + + if (response.Headers.Contains("Retry-After")) + { + var retryAfter = response.Headers.GetValues("Retry-After").First(); + if (retryAfter.Contains(':')) + { + var duration = DateTimeOffset.Parse(retryAfter).ToUniversalTime() - DateTimeOffset.Now.ToUniversalTime(); + await Task.Delay(duration); + } + else + { + await Task.Delay(int.Parse(retryAfter) * 1000); + } + } + else if (response.StatusCode is HttpStatusCode.BadGateway or HttpStatusCode.ServiceUnavailable or HttpStatusCode.GatewayTimeout) + { + await Task.Delay(TimeSpan.FromSeconds(Math.Ceiling(attempt * 1.5))); + } + else + { + response.EnsureSuccessStatusCode(); + } + + attempt++; + } + else + { + return response; + } } } }