feat(http-client): extend HTTP client with facilities for more manual response processing
ci/woodpecker/push/woodpecker Pipeline was successful
Details
ci/woodpecker/push/woodpecker Pipeline was successful
Details
This commit is contained in:
parent
067f3b9de9
commit
7a0d2f726e
|
@ -1,5 +1,6 @@
|
|||
package io.gitlab.jfronny.commons.http.client;
|
||||
|
||||
import io.gitlab.jfronny.commons.data.Either;
|
||||
import io.gitlab.jfronny.commons.serialize.Serializer;
|
||||
|
||||
import java.io.*;
|
||||
|
@ -60,6 +61,10 @@ public class HttpClient {
|
|||
DELETE
|
||||
}
|
||||
|
||||
private enum ResponseHandlingMode {
|
||||
IGNORE_ALL, HANDLE_REDIRECTS, HANDLE_ALL
|
||||
}
|
||||
|
||||
public static class Request {
|
||||
private static final Predicate<String> CURSEFORGE_API = Pattern.compile("(?:http(s)?://)?addons-ecs\\.forgesvc\\.net/api/+").asMatchPredicate();
|
||||
private final String url;
|
||||
|
@ -69,9 +74,10 @@ public class HttpClient {
|
|||
private int retryAfterDefault = 5000;
|
||||
private int retryAfterMax = 15000;
|
||||
private int retryLimit = 3;
|
||||
private ResponseHandlingMode responseHandlingMode = ResponseHandlingMode.HANDLE_ALL;
|
||||
private List<Exception> retryExceptions = null;
|
||||
|
||||
public Request(Method method, String url) throws URISyntaxException {
|
||||
private Request(Method method, String url) throws URISyntaxException {
|
||||
this.url = url.replace(" ", "%20");
|
||||
this.builder = HttpRequest.newBuilder()
|
||||
.uri(new URI(this.url));
|
||||
|
@ -95,6 +101,16 @@ public class HttpClient {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Request ignoreAll() {
|
||||
responseHandlingMode = ResponseHandlingMode.IGNORE_ALL;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Request handleRedirects() {
|
||||
responseHandlingMode = ResponseHandlingMode.HANDLE_REDIRECTS;
|
||||
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");
|
||||
|
@ -153,7 +169,7 @@ public class HttpClient {
|
|||
return this;
|
||||
}
|
||||
|
||||
private <T> T _send(String accept, HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException {
|
||||
private <T> HttpResponse<T> _sendResponse(String accept, HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException {
|
||||
sent++;
|
||||
if (sent > retryLimit) {
|
||||
IOException e = new IOException("Attempted to reconnect/redirect " + sent + " times, which is more than the permitted " + retryLimit + ". Stopping");
|
||||
|
@ -175,7 +191,8 @@ public class HttpClient {
|
|||
return handleRetryAfter(accept, responseBodyHandler, retryAfterDefault);
|
||||
} else throw new IOException("Could not send request", e);
|
||||
}
|
||||
if (res.statusCode() / 100 == 2) return res.body();
|
||||
if (responseHandlingMode == ResponseHandlingMode.IGNORE_ALL) return res;
|
||||
if (res.statusCode() / 100 == 2) return res;
|
||||
Optional<String> location = res.headers().firstValue("location");
|
||||
Optional<Integer> retryAfter = res.headers().firstValue("Retry-After").flatMap(s -> {
|
||||
try {
|
||||
|
@ -194,70 +211,96 @@ public class HttpClient {
|
|||
// Redirect
|
||||
if (location.isPresent() && method == Method.GET) {
|
||||
try {
|
||||
yield get(location.get())._send(accept, responseBodyHandler);
|
||||
yield get(location.get())._sendResponse(accept, responseBodyHandler);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IOException("Could not follow redirect" + exceptionSuffix, e);
|
||||
}
|
||||
}
|
||||
if (responseHandlingMode == ResponseHandlingMode.HANDLE_REDIRECTS) yield res;
|
||||
throw new IOException("Unexpected redirect: " + res.statusCode() + exceptionSuffix);
|
||||
}
|
||||
case 500, 502, 503, 504, 507 -> {
|
||||
if (responseHandlingMode == ResponseHandlingMode.HANDLE_REDIRECTS) yield res;
|
||||
// CurseForge serverside error
|
||||
if (CURSEFORGE_API.test(url)) {
|
||||
yield handleRetryAfter(accept, responseBodyHandler, Math.min(1000, retryAfterMax));
|
||||
}
|
||||
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);
|
||||
case 404 -> {
|
||||
if (responseHandlingMode == ResponseHandlingMode.HANDLE_REDIRECTS) yield res;
|
||||
throw new FileNotFoundException("Didn't find anything under that url" + exceptionSuffix);
|
||||
}
|
||||
default -> {
|
||||
if (responseHandlingMode == ResponseHandlingMode.HANDLE_REDIRECTS) yield res;
|
||||
throw new IOException("Unexpected return method: " + res.statusCode() + exceptionSuffix);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private <T> T handleRetryAfter(String accept, HttpResponse.BodyHandler<T> responseBodyHandler, int millis) throws IOException {
|
||||
private <T> HttpResponse<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);
|
||||
}
|
||||
return this._send(accept, responseBodyHandler);
|
||||
return this._sendResponse(accept, responseBodyHandler);
|
||||
}
|
||||
|
||||
private <T> T unwrap(HttpResponse<T> response) throws IOException {
|
||||
return response.body();
|
||||
}
|
||||
|
||||
public void send() throws IOException {
|
||||
_send("*/*", HttpResponse.BodyHandlers.discarding());
|
||||
unwrap(sendResponse());
|
||||
}
|
||||
|
||||
public HttpResponse<Void> sendResponse() throws IOException {
|
||||
return _sendResponse("*/*", HttpResponse.BodyHandlers.discarding());
|
||||
}
|
||||
|
||||
public InputStream sendInputStream() throws IOException {
|
||||
return _send("*/*", HttpResponse.BodyHandlers.ofInputStream());
|
||||
return unwrap(sendInputStreamResponse());
|
||||
}
|
||||
|
||||
public HttpResponse<InputStream> sendInputStreamResponse() throws IOException {
|
||||
return _sendResponse("*/*", HttpResponse.BodyHandlers.ofInputStream());
|
||||
}
|
||||
|
||||
public Reader sendReader() throws IOException {
|
||||
return new InputStreamReader(sendInputStream());
|
||||
return unwrap(sendReaderResponse());
|
||||
}
|
||||
|
||||
public HttpResponse<Reader> sendReaderResponse() throws IOException {
|
||||
return _sendResponse("*/*", ReaderHandler.of());
|
||||
}
|
||||
|
||||
public String sendString() throws IOException {
|
||||
return _send("*/*", HttpResponse.BodyHandlers.ofString());
|
||||
return unwrap(sendStringResponse());
|
||||
}
|
||||
|
||||
public HttpResponse<String> sendStringResponse() throws IOException {
|
||||
return _sendResponse("*/*", HttpResponse.BodyHandlers.ofString());
|
||||
}
|
||||
|
||||
public Stream<String> sendLines() throws IOException {
|
||||
return _send("*/*", HttpResponse.BodyHandlers.ofLines());
|
||||
return unwrap(sendLinesResponse());
|
||||
}
|
||||
|
||||
public HttpResponse<Stream<String>> sendLinesResponse() throws IOException {
|
||||
return _sendResponse("*/*", HttpResponse.BodyHandlers.ofLines());
|
||||
}
|
||||
|
||||
public <T> T sendSerialized(Type type) throws IOException {
|
||||
Serializer serializer = Serializer.getInstance();
|
||||
InputStream in = _send(serializer.getFormatMime(), HttpResponse.BodyHandlers.ofInputStream());
|
||||
try {
|
||||
return in == null ? null : serializer.deserialize(new InputStreamReader(in), type);
|
||||
} catch (Serializer.SerializeException e) {
|
||||
throw new IOException("Could not deserialize", e);
|
||||
}
|
||||
Either<T, IOException> tmp = unwrap(sendSerializedResponse(type));
|
||||
if (tmp == null) return null;
|
||||
if (tmp.isLeft()) return tmp.left();
|
||||
throw new IOException("Could not deserialize", tmp.right());
|
||||
}
|
||||
|
||||
private String getString(Object a) throws IOException {
|
||||
if (a instanceof InputStream s) return new String(s.readAllBytes());
|
||||
if (a instanceof String s) return s;
|
||||
if (a instanceof Stream s) return ((Stream<String>) s).collect(Collectors.joining());
|
||||
return "";
|
||||
public <T> HttpResponse<Either<T, IOException>> sendSerializedResponse(Type type) throws IOException {
|
||||
return _sendResponse(Serializer.getInstance().getFormatMime(), SerializedHandler.of(Serializer.getInstance(), type));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package io.gitlab.jfronny.commons.http.client;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.Flow;
|
||||
|
||||
public record ReaderHandler(HttpResponse.BodySubscriber<InputStream> delegate) implements HttpResponse.BodySubscriber<Reader> {
|
||||
public static HttpResponse.BodyHandler<Reader> of() {
|
||||
return responseInfo -> new ReaderHandler(HttpResponse.BodySubscribers.ofInputStream());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Reader> getBody() {
|
||||
return this.delegate.getBody().thenApply(in -> in == null ? null : new InputStreamReader(in));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubscribe(Flow.Subscription subscription) {
|
||||
this.delegate.onSubscribe(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(List<ByteBuffer> item) {
|
||||
this.delegate.onNext(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
this.delegate.onError(throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
this.delegate.onComplete();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package io.gitlab.jfronny.commons.http.client;
|
||||
|
||||
import io.gitlab.jfronny.commons.data.Either;
|
||||
import io.gitlab.jfronny.commons.serialize.Serializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.Flow;
|
||||
|
||||
public record SerializedHandler<T>(HttpResponse.BodySubscriber<Reader> delegate, Serializer serializer, Type type) implements HttpResponse.BodySubscriber<Either<T, IOException>> {
|
||||
public static <T> HttpResponse.BodyHandler<Either<T, IOException>> of(Serializer serializer, Type type) {
|
||||
return responseInfo -> new SerializedHandler<>(new ReaderHandler(HttpResponse.BodySubscribers.ofInputStream()), serializer, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Either<T, IOException>> getBody() {
|
||||
return this.delegate.getBody().thenApply(in -> {
|
||||
try {
|
||||
return Either.left(in == null ? null : this.serializer.<T>deserialize(in, this.type));
|
||||
} catch (IOException e) {
|
||||
return Either.right(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubscribe(Flow.Subscription subscription) {
|
||||
this.delegate.onSubscribe(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(List<ByteBuffer> byteBuffers) {
|
||||
this.delegate.onNext(byteBuffers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
this.delegate.onError(throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
this.delegate.onComplete();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue