retry hCaptcha errors
Co-authored-by: Jon Chambers <63609320+jon-signal@users.noreply.github.com>
This commit is contained in:
parent
b594986241
commit
0fa8276d2d
|
@ -406,6 +406,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
.scheduledExecutorService(name(getClass(), "secureValueRecoveryServiceRetry-%d")).threads(1).build();
|
.scheduledExecutorService(name(getClass(), "secureValueRecoveryServiceRetry-%d")).threads(1).build();
|
||||||
ScheduledExecutorService storageServiceRetryExecutor = environment.lifecycle()
|
ScheduledExecutorService storageServiceRetryExecutor = environment.lifecycle()
|
||||||
.scheduledExecutorService(name(getClass(), "storageServiceRetry-%d")).threads(1).build();
|
.scheduledExecutorService(name(getClass(), "storageServiceRetry-%d")).threads(1).build();
|
||||||
|
ScheduledExecutorService hcaptchaRetryExecutor = environment.lifecycle()
|
||||||
|
.scheduledExecutorService(name(getClass(), "hCaptchaRetry-%d")).threads(1).build();
|
||||||
|
|
||||||
Scheduler messageDeliveryScheduler = Schedulers.fromExecutorService(
|
Scheduler messageDeliveryScheduler = Schedulers.fromExecutorService(
|
||||||
ExecutorServiceMetrics.monitor(Metrics.globalRegistry,
|
ExecutorServiceMetrics.monitor(Metrics.globalRegistry,
|
||||||
|
@ -569,9 +571,11 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
config.getRecaptchaConfiguration().projectPath(),
|
config.getRecaptchaConfiguration().projectPath(),
|
||||||
config.getRecaptchaConfiguration().credentialConfigurationJson(),
|
config.getRecaptchaConfiguration().credentialConfigurationJson(),
|
||||||
dynamicConfigurationManager);
|
dynamicConfigurationManager);
|
||||||
HttpClient hcaptchaHttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2)
|
HCaptchaClient hCaptchaClient = new HCaptchaClient(
|
||||||
.connectTimeout(Duration.ofSeconds(10)).build();
|
config.getHCaptchaConfiguration().getApiKey().value(),
|
||||||
HCaptchaClient hCaptchaClient = new HCaptchaClient(config.getHCaptchaConfiguration().apiKey().value(), hcaptchaHttpClient,
|
hcaptchaRetryExecutor,
|
||||||
|
config.getHCaptchaConfiguration().getCircuitBreaker(),
|
||||||
|
config.getHCaptchaConfiguration().getRetry(),
|
||||||
dynamicConfigurationManager);
|
dynamicConfigurationManager);
|
||||||
HttpClient shortCodeRetrieverHttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2)
|
HttpClient shortCodeRetrieverHttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2)
|
||||||
.connectTimeout(Duration.ofSeconds(10)).build();
|
.connectTimeout(Duration.ofSeconds(10)).build();
|
||||||
|
|
|
@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.captcha;
|
||||||
|
|
||||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
import io.micrometer.core.instrument.Metrics;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
@ -16,15 +17,25 @@ import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.RetryConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicCaptchaConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicCaptchaConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||||
|
|
||||||
public class HCaptchaClient implements CaptchaClient {
|
public class HCaptchaClient implements CaptchaClient {
|
||||||
|
@ -34,16 +45,36 @@ public class HCaptchaClient implements CaptchaClient {
|
||||||
private static final String ASSESSMENT_REASON_COUNTER_NAME = name(HCaptchaClient.class, "assessmentReason");
|
private static final String ASSESSMENT_REASON_COUNTER_NAME = name(HCaptchaClient.class, "assessmentReason");
|
||||||
private static final String INVALID_REASON_COUNTER_NAME = name(HCaptchaClient.class, "invalidReason");
|
private static final String INVALID_REASON_COUNTER_NAME = name(HCaptchaClient.class, "invalidReason");
|
||||||
private final String apiKey;
|
private final String apiKey;
|
||||||
private final HttpClient client;
|
private final FaultTolerantHttpClient client;
|
||||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
HCaptchaClient(final String apiKey,
|
||||||
|
final FaultTolerantHttpClient faultTolerantHttpClient,
|
||||||
|
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
this.client = faultTolerantHttpClient;
|
||||||
|
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||||
|
}
|
||||||
|
|
||||||
public HCaptchaClient(
|
public HCaptchaClient(
|
||||||
final String apiKey,
|
final String apiKey,
|
||||||
final HttpClient client,
|
final ScheduledExecutorService retryExecutor,
|
||||||
|
final CircuitBreakerConfiguration circuitBreakerConfiguration,
|
||||||
|
final RetryConfiguration retryConfiguration,
|
||||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
|
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
|
||||||
this.apiKey = apiKey;
|
this(apiKey,
|
||||||
this.client = client;
|
FaultTolerantHttpClient.newBuilder()
|
||||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
.withName("hcaptcha")
|
||||||
|
.withCircuitBreaker(circuitBreakerConfiguration)
|
||||||
|
.withExecutor(Executors.newCachedThreadPool())
|
||||||
|
.withRetryExecutor(retryExecutor)
|
||||||
|
.withRetry(retryConfiguration)
|
||||||
|
.withRetryOnException(ex -> ex instanceof IOException)
|
||||||
|
.withConnectTimeout(Duration.ofSeconds(10))
|
||||||
|
.withVersion(HttpClient.Version.HTTP_2)
|
||||||
|
.build(),
|
||||||
|
dynamicConfigurationManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -82,11 +113,12 @@ public class HCaptchaClient implements CaptchaClient {
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(body))
|
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response;
|
final HttpResponse<String> response;
|
||||||
try {
|
try {
|
||||||
response = this.client.send(request, HttpResponse.BodyHandlers.ofString());
|
response = this.client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join();
|
||||||
} catch (InterruptedException e) {
|
} catch (CompletionException e) {
|
||||||
throw new IOException(e);
|
logger.warn("failed to make http request to hCaptcha: {}", e.getMessage());
|
||||||
|
throw new IOException(ExceptionUtils.unwrap(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.statusCode() != Response.Status.OK.getStatusCode()) {
|
if (response.statusCode() != Response.Status.OK.getStatusCode()) {
|
||||||
|
|
|
@ -5,8 +5,35 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.configuration;
|
package org.whispersystems.textsecuregcm.configuration;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
||||||
|
|
||||||
public record HCaptchaConfiguration(@NotNull SecretString apiKey) {
|
public class HCaptchaConfiguration {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
SecretString apiKey;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
RetryConfiguration retry = new RetryConfiguration();
|
||||||
|
|
||||||
|
|
||||||
|
public SecretString getApiKey() {
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CircuitBreakerConfiguration getCircuitBreaker() {
|
||||||
|
return circuitBreaker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RetryConfiguration getRetry() {
|
||||||
|
return retry;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.http;
|
package org.whispersystems.textsecuregcm.http;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
|
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
|
||||||
import io.github.resilience4j.retry.Retry;
|
import io.github.resilience4j.retry.Retry;
|
||||||
import io.github.resilience4j.retry.RetryConfig;
|
import io.github.resilience4j.retry.RetryConfig;
|
||||||
|
@ -18,12 +19,14 @@ import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionStage;
|
import java.util.concurrent.CompletionStage;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import org.glassfish.jersey.SslConfigurator;
|
import org.glassfish.jersey.SslConfigurator;
|
||||||
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.RetryConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.RetryConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.util.CertificateUtil;
|
import org.whispersystems.textsecuregcm.util.CertificateUtil;
|
||||||
import org.whispersystems.textsecuregcm.util.CircuitBreakerUtil;
|
import org.whispersystems.textsecuregcm.util.CircuitBreakerUtil;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||||
|
|
||||||
public class FaultTolerantHttpClient {
|
public class FaultTolerantHttpClient {
|
||||||
|
|
||||||
|
@ -40,9 +43,10 @@ public class FaultTolerantHttpClient {
|
||||||
return new Builder();
|
return new Builder();
|
||||||
}
|
}
|
||||||
|
|
||||||
private FaultTolerantHttpClient(String name, HttpClient httpClient, ScheduledExecutorService retryExecutor,
|
@VisibleForTesting
|
||||||
|
FaultTolerantHttpClient(String name, HttpClient httpClient, ScheduledExecutorService retryExecutor,
|
||||||
Duration defaultRequestTimeout, RetryConfiguration retryConfiguration,
|
Duration defaultRequestTimeout, RetryConfiguration retryConfiguration,
|
||||||
CircuitBreakerConfiguration circuitBreakerConfiguration) {
|
final Predicate<Throwable> retryOnException, CircuitBreakerConfiguration circuitBreakerConfiguration) {
|
||||||
|
|
||||||
this.httpClient = httpClient;
|
this.httpClient = httpClient;
|
||||||
this.retryExecutor = retryExecutor;
|
this.retryExecutor = retryExecutor;
|
||||||
|
@ -55,9 +59,12 @@ public class FaultTolerantHttpClient {
|
||||||
if (this.retryExecutor == null) {
|
if (this.retryExecutor == null) {
|
||||||
throw new IllegalArgumentException("retryExecutor must be specified with retryConfiguration");
|
throw new IllegalArgumentException("retryExecutor must be specified with retryConfiguration");
|
||||||
}
|
}
|
||||||
RetryConfig retryConfig = retryConfiguration.<HttpResponse>toRetryConfigBuilder()
|
final RetryConfig.Builder<HttpResponse> retryConfig = retryConfiguration.<HttpResponse>toRetryConfigBuilder()
|
||||||
.retryOnResult(o -> o.statusCode() >= 500).build();
|
.retryOnResult(o -> o.statusCode() >= 500);
|
||||||
this.retry = Retry.of(name + "-retry", retryConfig);
|
if (retryOnException != null) {
|
||||||
|
retryConfig.retryOnException(retryOnException);
|
||||||
|
}
|
||||||
|
this.retry = Retry.of(name + "-retry", retryConfig.build());
|
||||||
CircuitBreakerUtil.registerMetrics(retry, FaultTolerantHttpClient.class);
|
CircuitBreakerUtil.registerMetrics(retry, FaultTolerantHttpClient.class);
|
||||||
} else {
|
} else {
|
||||||
this.retry = null;
|
this.retry = null;
|
||||||
|
@ -101,6 +108,7 @@ public class FaultTolerantHttpClient {
|
||||||
private KeyStore trustStore;
|
private KeyStore trustStore;
|
||||||
private String securityProtocol = SECURITY_PROTOCOL_TLS_1_2;
|
private String securityProtocol = SECURITY_PROTOCOL_TLS_1_2;
|
||||||
private RetryConfiguration retryConfiguration;
|
private RetryConfiguration retryConfiguration;
|
||||||
|
private Predicate<Throwable> retryOnException;
|
||||||
private CircuitBreakerConfiguration circuitBreakerConfiguration;
|
private CircuitBreakerConfiguration circuitBreakerConfiguration;
|
||||||
|
|
||||||
private Builder() {
|
private Builder() {
|
||||||
|
@ -161,6 +169,11 @@ public class FaultTolerantHttpClient {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder withRetryOnException(final Predicate<Throwable> predicate) {
|
||||||
|
this.retryOnException = throwable -> predicate.test(ExceptionUtils.unwrap(throwable));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public FaultTolerantHttpClient build() {
|
public FaultTolerantHttpClient build() {
|
||||||
if (this.circuitBreakerConfiguration == null || this.name == null || this.executor == null) {
|
if (this.circuitBreakerConfiguration == null || this.name == null || this.executor == null) {
|
||||||
throw new IllegalArgumentException("Must specify circuit breaker config, name, and executor");
|
throw new IllegalArgumentException("Must specify circuit breaker config, name, and executor");
|
||||||
|
@ -181,7 +194,7 @@ public class FaultTolerantHttpClient {
|
||||||
builder.sslContext(sslConfigurator.createSSLContext());
|
builder.sslContext(sslConfigurator.createSSLContext());
|
||||||
|
|
||||||
return new FaultTolerantHttpClient(name, builder.build(), retryExecutor, requestTimeout, retryConfiguration,
|
return new FaultTolerantHttpClient(name, builder.build(), retryExecutor, requestTimeout, retryConfiguration,
|
||||||
circuitBreakerConfiguration);
|
retryOnException, circuitBreakerConfiguration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
@ -22,6 +23,7 @@ import org.junit.jupiter.params.provider.Arguments;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicCaptchaConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicCaptchaConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
|
|
||||||
public class HCaptchaClientTest {
|
public class HCaptchaClientTest {
|
||||||
|
@ -44,7 +46,7 @@ public class HCaptchaClientTest {
|
||||||
public void captchaProcessed(final boolean success, final float score, final boolean expectedResult)
|
public void captchaProcessed(final boolean success, final float score, final boolean expectedResult)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
|
|
||||||
final HttpClient client = mockResponder(200, String.format("""
|
final FaultTolerantHttpClient client = mockResponder(200, String.format("""
|
||||||
{
|
{
|
||||||
"success": %b,
|
"success": %b,
|
||||||
"score": %f,
|
"score": %f,
|
||||||
|
@ -64,14 +66,14 @@ public class HCaptchaClientTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void errorResponse() throws IOException, InterruptedException {
|
public void errorResponse() throws IOException, InterruptedException {
|
||||||
final HttpClient httpClient = mockResponder(503, "");
|
final FaultTolerantHttpClient httpClient = mockResponder(503, "");
|
||||||
final HCaptchaClient client = new HCaptchaClient("fake", httpClient, mockConfig(true, 0.5));
|
final HCaptchaClient client = new HCaptchaClient("fake", httpClient, mockConfig(true, 0.5));
|
||||||
assertThrows(IOException.class, () -> client.verify(SITE_KEY, Action.CHALLENGE, TOKEN, null));
|
assertThrows(IOException.class, () -> client.verify(SITE_KEY, Action.CHALLENGE, TOKEN, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void invalidScore() throws IOException, InterruptedException {
|
public void invalidScore() throws IOException, InterruptedException {
|
||||||
final HttpClient httpClient = mockResponder(200, """
|
final FaultTolerantHttpClient httpClient = mockResponder(200, """
|
||||||
{"success" : true, "score": 1.1}
|
{"success" : true, "score": 1.1}
|
||||||
""");
|
""");
|
||||||
final HCaptchaClient client = new HCaptchaClient("fake", httpClient, mockConfig(true, 0.5));
|
final HCaptchaClient client = new HCaptchaClient("fake", httpClient, mockConfig(true, 0.5));
|
||||||
|
@ -80,7 +82,7 @@ public class HCaptchaClientTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void badBody() throws IOException, InterruptedException {
|
public void badBody() throws IOException, InterruptedException {
|
||||||
final HttpClient httpClient = mockResponder(200, """
|
final FaultTolerantHttpClient httpClient = mockResponder(200, """
|
||||||
{"success" : true,
|
{"success" : true,
|
||||||
""");
|
""");
|
||||||
final HCaptchaClient client = new HCaptchaClient("fake", httpClient, mockConfig(true, 0.5));
|
final HCaptchaClient client = new HCaptchaClient("fake", httpClient, mockConfig(true, 0.5));
|
||||||
|
@ -102,15 +104,14 @@ public class HCaptchaClientTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpClient mockResponder(final int statusCode, final String jsonBody)
|
private static FaultTolerantHttpClient mockResponder(final int statusCode, final String jsonBody) {
|
||||||
throws IOException, InterruptedException {
|
FaultTolerantHttpClient httpClient = mock(FaultTolerantHttpClient.class);
|
||||||
HttpClient httpClient = mock(HttpClient.class);
|
|
||||||
@SuppressWarnings("unchecked") final HttpResponse<Object> httpResponse = mock(HttpResponse.class);
|
@SuppressWarnings("unchecked") final HttpResponse<Object> httpResponse = mock(HttpResponse.class);
|
||||||
|
|
||||||
when(httpResponse.body()).thenReturn(jsonBody);
|
when(httpResponse.body()).thenReturn(jsonBody);
|
||||||
when(httpResponse.statusCode()).thenReturn(statusCode);
|
when(httpResponse.statusCode()).thenReturn(statusCode);
|
||||||
|
|
||||||
when(httpClient.send(any(), any())).thenReturn(httpResponse);
|
when(httpClient.sendAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(httpResponse));
|
||||||
return httpClient;
|
return httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,11 @@ import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
|
||||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
|
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
|
||||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
|
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
|
||||||
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
|
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
|
||||||
|
@ -19,6 +24,8 @@ import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
@ -114,6 +121,35 @@ class FaultTolerantHttpClientTest {
|
||||||
wireMock.verify(3, getRequestedFor(urlEqualTo("/failure")));
|
wireMock.verify(3, getRequestedFor(urlEqualTo("/failure")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRetryGetOnException() {
|
||||||
|
final HttpClient mockHttpClient = mock(HttpClient.class);
|
||||||
|
final FaultTolerantHttpClient client = new FaultTolerantHttpClient(
|
||||||
|
"test",
|
||||||
|
mockHttpClient,
|
||||||
|
retryExecutor,
|
||||||
|
Duration.ofSeconds(1),
|
||||||
|
new RetryConfiguration(),
|
||||||
|
throwable -> throwable instanceof IOException,
|
||||||
|
new CircuitBreakerConfiguration());
|
||||||
|
|
||||||
|
when(mockHttpClient.sendAsync(any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.failedFuture(new IOException("test exception")));
|
||||||
|
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create("http://localhost:1234/failure"))
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join();
|
||||||
|
throw new AssertionError("Should have failed!");
|
||||||
|
} catch (CompletionException e) {
|
||||||
|
assertThat(e.getCause()).isInstanceOf(IOException.class);
|
||||||
|
}
|
||||||
|
verify(mockHttpClient, times(3)).sendAsync(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testNetworkFailureCircuitBreaker() throws InterruptedException {
|
void testNetworkFailureCircuitBreaker() throws InterruptedException {
|
||||||
CircuitBreakerConfiguration circuitBreakerConfiguration = new CircuitBreakerConfiguration();
|
CircuitBreakerConfiguration circuitBreakerConfiguration = new CircuitBreakerConfiguration();
|
||||||
|
|
Loading…
Reference in New Issue