diff --git a/unirest-bdd-tests/src/test/java/BehaviorTests/RetryAsyncTest.java b/unirest-bdd-tests/src/test/java/BehaviorTests/RetryAsyncTest.java new file mode 100644 index 00000000..e35d80f1 --- /dev/null +++ b/unirest-bdd-tests/src/test/java/BehaviorTests/RetryAsyncTest.java @@ -0,0 +1,174 @@ +/** + * The MIT License + * + * Copyright for portions of unirest-java are held by Kong Inc (c) 2013. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package BehaviorTests; + +import kong.unirest.core.*; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RetryAsyncTest extends BddTest { + + @Test + void whenSettingIsOff() { + MockServer.retryTimes(100, 429, 1d); + assertEquals(429, Unirest.get(MockServer.GET).asEmpty().getStatus()); + assertEquals(429, Unirest.get(MockServer.GET).asString().getStatus()); + assertEquals(429, Unirest.get(MockServer.GET).asBytes().getStatus()); + assertEquals(429, Unirest.get(MockServer.GET).asObject(RequestCapture.class).getStatus()); + } + + @Test + void dontRetry_whenHeaderIsMissing(){ + MockServer.retryTimes(100, 429, (String)null); + assertDidNotRetry(); + } + + @Test + void dontRetry_whenHeaderIsUnparseable(){ + MockServer.retryTimes(100, 429, "David S Pumpkins"); + assertDidNotRetry(); + } + + @Test + void dontRetry_whenHeaderIsNegative(){ + MockServer.retryTimes(100, 429, -5.5); + assertDidNotRetry(); + } + + @Test + void dontRetry_whenHeaderIsEmpty(){ + MockServer.retryTimes(100, 429, ""); + assertDidNotRetry(); + } + + @Test + void willRetryAfterSeconds_AsObject() { + this.doWithRetry(r -> r.asObjectAsync(RequestCapture.class)) + .assertMethod(HttpMethod.GET); + } + + @Test + void willRetryAfterSeconds_AsObjectGenerics() { + List o = Arrays.asList( + new Foo("foo"), + new Foo("bar"), + new Foo("baz") + ); + MockServer.setJsonAsResponse(o); + List cap = (List)this.doWithRetry(r -> r.asObjectAsync(new GenericType>(){})); + assertEquals(o, cap); + } + + @Test + void willRetryAfterSeconds_AsString() { + MockServer.setStringResponse("Hi Mom"); + String cap = this.doWithRetry(r -> r.asStringAsync()); + assertEquals("Hi Mom", cap); + } + + @Test + void willRetryAfterSeconds_AsJson() { + MockServer.setStringResponse("{\"message\": \"Hi Mom\"}"); + JsonNode cap = this.doWithRetry(r -> r.asJsonAsync()); + assertEquals("Hi Mom", cap.getObject().getString("message")); + } + + @Test + void willRetryAfterSeconds_AsBytes() { + MockServer.setStringResponse("Hi Mom"); + byte[] cap = this.doWithRetry(r -> r.asBytesAsync()); + assertEquals("Hi Mom", new String(cap)); + } + + @Test + void willRetryAfterSeconds_Empty() { + doWithRetry(r -> r.asEmptyAsync()); + } + + @Test + void willRetryAfterSeconds_AsFile() { + Path path = Paths.get("results.json"); + clearFile(path); + + MockServer.setStringResponse("Hi Mom"); + File cap = (File)this.doWithRetry(r -> r.asFileAsync(path.toString(), StandardCopyOption.REPLACE_EXISTING)); + assertTrue(cap.exists()); + + clearFile(path); + } + + @Test + void willReturn429OnceItExceedsMaxAttempts() { + MockServer.retryTimes(10, 429, .01); + Unirest.config().retryAfter(true, 3); + + HttpResponse resp = Unirest.get(MockServer.GET).asEmpty(); + assertEquals(429, resp.getStatus()); + MockServer.assertRequestCount(3); + } + + private void clearFile(Path path) { + try { + Files.delete(path); + } catch (Exception ignored) { } + } + + private void assertDidNotRetry() { + Unirest.config().retryAfter(true); + assertEquals(429, Unirest.get(MockServer.GET).asEmpty().getStatus()); + MockServer.assertRequestCount(1); + } + + private R doWithRetry(Function>> bodyExtractor) { + try { + MockServer.retryTimes(1, 429, 0.01); + + Unirest.config().retryAfter(true); + + HttpRequest request = Unirest.get(MockServer.GET); + + HttpResponse response = bodyExtractor.apply(request).get(); + assertEquals(200, response.getStatus()); + MockServer.assertRequestCount(2); + + return response.getBody(); + }catch (Exception e){ + throw new AssertionError(e); + } + } +} diff --git a/unirest/src/main/java/kong/unirest/core/BaseRequest.java b/unirest/src/main/java/kong/unirest/core/BaseRequest.java index db779f68..2688adb0 100644 --- a/unirest/src/main/java/kong/unirest/core/BaseRequest.java +++ b/unirest/src/main/java/kong/unirest/core/BaseRequest.java @@ -28,7 +28,10 @@ import java.io.File; import java.nio.file.CopyOption; import java.time.Instant; -import java.util.*; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.function.Function; @@ -337,8 +340,8 @@ public PagedList asPaged(Function mappingFunct private HttpResponse request(Function> transformer, Class resultType){ HttpResponse response = config.getClient().request(this, transformer, resultType); - callCount++; if(config.isAutomaticRetryAfter()) { + callCount++; var retryAfter = config.getRetryStrategy(); if(retryAfter.isRetryable(response) && callCount < config.maxRetries()) { long waitTime = retryAfter.getWaitTime(response); @@ -355,7 +358,26 @@ private CompletableFuture> requestAsync(HttpRequest request, Function> transformer, CompletableFuture> callback, Class resultType){ - return config.getClient().request(request, transformer, callback, resultType); + var asyncR = config.getClient().request(request, transformer, callback, resultType); + if(config.isAutomaticRetryAfter()){ + return asyncR.thenApplyAsync(response -> { + callCount++; + var retryAfter = config.getRetryStrategy(); + if(retryAfter.isRetryable(response) && callCount < config.maxRetries()) { + long waitTime = retryAfter.getWaitTime(response); + if (waitTime > 0) { + retryAfter.waitFor(waitTime); + try { + return requestAsync(this, transformer, callback, resultType).get(); + } catch (Exception e) { + throw new UnirestException(e); + } + } + } + return response; + }); + } + return asyncR; } diff --git a/unirest/src/main/java/kong/unirest/core/Client.java b/unirest/src/main/java/kong/unirest/core/Client.java index 2e958e87..d356988b 100644 --- a/unirest/src/main/java/kong/unirest/core/Client.java +++ b/unirest/src/main/java/kong/unirest/core/Client.java @@ -29,6 +29,9 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Function; +/** + * The client that does the work. + */ public interface Client { /** * @param the underlying client @@ -60,5 +63,11 @@ CompletableFuture> request(HttpRequest request, CompletableFuture> callback, Class resultType); + /** + * Create a websocket connection + * @param request the connection + * @param listener (in the voice of Cicero) the listener + * @return a WebSocketResponse + */ WebSocketResponse websocket(WebSocketRequest request, WebSocket.Listener listener); } diff --git a/unirest/src/main/java/kong/unirest/core/WebSocketResponse.java b/unirest/src/main/java/kong/unirest/core/WebSocketResponse.java index f7e3c044..d7ef8839 100644 --- a/unirest/src/main/java/kong/unirest/core/WebSocketResponse.java +++ b/unirest/src/main/java/kong/unirest/core/WebSocketResponse.java @@ -28,19 +28,33 @@ import java.net.http.WebSocket; import java.util.concurrent.CompletableFuture; +/** + * Just a silly little class that holds on to the socket and listener + */ public class WebSocketResponse { private final CompletableFuture webSocketFuture; private final WebSocket.Listener listener; + /** + * ctor + * @param webSocketFuture the ws future + * @param listener the listener + */ public WebSocketResponse(CompletableFuture webSocketFuture, WebSocket.Listener listener) { this.webSocketFuture = webSocketFuture; this.listener = listener; } + /** + * @return the ws future + */ public CompletableFuture socket(){ return webSocketFuture; } + /** + * @return the listener + */ public WebSocket.Listener listener(){ return listener; }