feat(http): handle 429 with Retry-After using seconds
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
4df45c8fad
commit
1c5e9ee1b9
@ -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> T _send(String accept, HttpResponse.BodyHandler<T> 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<String> location = res.headers().firstValue("location");
|
||||
// Redirect
|
||||
if (location.isPresent() && (res.statusCode() == 302 || res.statusCode() == 307) && method == Method.GET) {
|
||||
Optional<Integer> 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> T handleRetryAfter(String accept, HttpResponse.BodyHandler<T> 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 {
|
||||
|
Loading…
Reference in New Issue
Block a user