From 1c5e9ee1b99708fb48b93b87f180e5a3fca9e329 Mon Sep 17 00:00:00 2001 From: JFronny Date: Fri, 28 Jul 2023 16:30:09 +0200 Subject: [PATCH] feat(http): handle 429 with Retry-After using seconds --- .../io/gitlab/jfronny/commons/HttpUtils.java | 75 ++++++++++++++----- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/src/main/java/io/gitlab/jfronny/commons/HttpUtils.java b/src/main/java/io/gitlab/jfronny/commons/HttpUtils.java index f8178eb..a62d360 100644 --- a/src/main/java/io/gitlab/jfronny/commons/HttpUtils.java +++ b/src/main/java/io/gitlab/jfronny/commons/HttpUtils.java @@ -64,6 +64,9 @@ public class HttpUtils { private final HttpRequest.Builder builder; private Method method; private int sent = 0; + private int retryAfterDefault = 5000; + private int retryAfterMax = 15000; + private int retryLimit = 3; public Request(Method method, String url) throws URISyntaxException { this.url = url.replace(" ", "%20"); @@ -89,6 +92,20 @@ public class HttpUtils { return this; } + public Request configureRetryAfter(int defaultDelay, int maxDelay) { + if (defaultDelay < 1) throw new IllegalArgumentException("defaultDelay must be greater than zero"); + if (maxDelay < defaultDelay) throw new IllegalArgumentException("maxDelay must be greater than or equal to defaultDelay"); + retryAfterDefault = defaultDelay; + retryAfterMax = maxDelay; + return this; + } + + public Request setRetryLimit(int limit) { + if (limit < 0) throw new IllegalArgumentException("limit must be greater than or zero"); + retryLimit = limit; + return this; + } + public Request userAgent(String value) { return setHeader("User-Agent", value); } @@ -135,7 +152,7 @@ public class HttpUtils { private T _send(String accept, HttpResponse.BodyHandler responseBodyHandler) throws IOException { sent++; - if (sent > 3) throw new IOException("Attempted third redirect, stopping"); + if (sent > retryLimit) throw new IOException("Attempted third redirect, stopping"); builder.header("Accept", accept); if (method != null) builder.method(method.name(), HttpRequest.BodyPublishers.noBody()); if (PROXY_AUTH != null) builder.header("Proxy-Authorization", PROXY_AUTH); @@ -148,26 +165,50 @@ public class HttpUtils { } if (res.statusCode() / 100 == 2) return res.body(); Optional location = res.headers().firstValue("location"); - // Redirect - if (location.isPresent() && (res.statusCode() == 302 || res.statusCode() == 307) && method == Method.GET) { + Optional retryAfter = res.headers().firstValue("Retry-After").flatMap(s -> { try { - return HttpUtils.get(location.get())._send(accept, responseBodyHandler); - } catch (URISyntaxException e) { - throw new IOException("Could not follow redirect", e); + return Optional.of(Integer.parseInt(s)); + } catch (NumberFormatException e) { + return Optional.empty(); } - } - // CurseForge serverside error - if (CURSEFORGE_API.test(url) && res.statusCode() >= 500 && res.statusCode() < 600) { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new IOException("Could not sleep before resending request" + e); + }); + final String exceptionSuffix = " (URL=" + url + ")"; + return switch (res.statusCode()) { + case 429 -> { + // Rate limit + yield handleRetryAfter(accept, responseBodyHandler, retryAfter.map(s -> s * 1000).orElse(retryAfterDefault)); } - return _send(accept, responseBodyHandler); + case 302, 307 -> { + // Redirect + if (location.isPresent() && method == Method.GET) { + try { + yield HttpUtils.get(location.get())._send(accept, responseBodyHandler); + } catch (URISyntaxException e) { + throw new IOException("Could not follow redirect" + exceptionSuffix, e); + } + } + throw new IOException("Unexpected redirect: " + res.statusCode() + exceptionSuffix); + } + case 500, 502, 503, 504, 507 -> { + // CurseForge serverside error + if (CURSEFORGE_API.test(url)) { + yield handleRetryAfter(accept, responseBodyHandler, 1000); + } + throw new IOException("Unexpected serverside error: " + res.statusCode() + exceptionSuffix); + } + case 404 -> throw new FileNotFoundException("Didn't find anything under that url" + exceptionSuffix); + default -> throw new IOException("Unexpected return method: " + res.statusCode() + exceptionSuffix); + }; + } + + private T handleRetryAfter(String accept, HttpResponse.BodyHandler responseBodyHandler, int millis) throws IOException { + if (millis > retryAfterMax) throw new HttpTimeoutException("Wait time specified by Retry-After is too long: " + millis); + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + throw new IOException("Could not sleep before resending request" + e); } - if (res.statusCode() == 404) - throw new FileNotFoundException("Didn't find anything under that url (URL=" + url + ")"); - throw new IOException("Unexpected return method: " + res.statusCode() + " (URL=" + url + ")"); + return this._send(accept, responseBodyHandler); } public void send() throws IOException {