Skip to content

Commit 9ef2c80

Browse files
authored
Merge pull request #208 from kadiiiri/configurable-request-timeout
Shapeshifter connection and read timeouts can be configured
2 parents f6b8687 + 49a85e3 commit 9ef2c80

7 files changed

Lines changed: 279 additions & 5 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.lfenergy.shapeshifter.core.service.sending;
2+
3+
import java.net.http.HttpRequest;
4+
import java.util.function.Consumer;
5+
6+
/**
7+
* Interceptor that can modify an {@link HttpRequest.Builder} before the request is built and sent.
8+
* <p>
9+
* Example usage:
10+
* <pre>{@code
11+
* service.addRequestInterceptor(request -> request.timeout(Duration.ofSeconds(30)));
12+
* }</pre>
13+
*/
14+
@FunctionalInterface
15+
public interface RequestInterceptor extends Consumer<HttpRequest.Builder> {
16+
17+
}

core/src/main/java/org/lfenergy/shapeshifter/core/service/sending/UftpSendMessageService.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package org.lfenergy.shapeshifter.core.service.sending;
66

7+
78
import lombok.NonNull;
89
import lombok.extern.apachecommons.CommonsLog;
910
import org.lfenergy.shapeshifter.api.PayloadMessageResponseType;
@@ -26,9 +27,7 @@
2627
import java.net.http.HttpRequest.BodyPublishers;
2728
import java.net.http.HttpResponse.BodyHandlers;
2829
import java.text.MessageFormat;
29-
import java.util.HashMap;
30-
import java.util.Map;
31-
import java.util.Set;
30+
import java.util.*;
3231

3332
/**
3433
* Sends UFTP messages to recipients
@@ -66,6 +65,9 @@ public class UftpSendMessageService {
6665
private final UftpValidationService uftpValidationService;
6766
private final HttpClient httpClient;
6867

68+
private final List<RequestInterceptor> requestInterceptors = new ArrayList<>();
69+
70+
6971
/**
7072
* Creates a new {@link UftpSendMessageService} with default HttpClient.
7173
*/
@@ -120,6 +122,16 @@ public void attemptToValidateAndSendMessage(@NonNull PayloadMessageType payloadM
120122
doSend(payloadMessage, details);
121123
}
122124

125+
/**
126+
* Registers a request interceptor that can modify each outgoing {@link HttpRequest.Builder}
127+
* before the request is built and sent.
128+
*
129+
* @param interceptor the interceptor to add
130+
*/
131+
public void addRequestInterceptor(@NonNull RequestInterceptor interceptor) {
132+
requestInterceptors.add(interceptor);
133+
}
134+
123135
private void doSend(PayloadMessageType payloadMessage, SigningDetails details) {
124136
String signedXml = getSignedXml(payloadMessage, details);
125137
UftpParticipantInformation participantInformation = participantService.getParticipantInformation(details.recipient());
@@ -148,6 +160,9 @@ private void send(String signedXml, String url, Map<String, String> additionalHe
148160
for (var header : additionalHeaders.entrySet()) {
149161
requestBuilder.setHeader(header.getKey(), header.getValue());
150162
}
163+
164+
requestInterceptors.forEach(interceptor -> interceptor.accept(requestBuilder));
165+
151166
var request = requestBuilder.build();
152167

153168
var response = httpClient.send(request, BodyHandlers.ofString());

core/src/test/java/org/lfenergy/shapeshifter/core/service/sending/UftpSendMessageServiceTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,15 @@
3737
import org.lfenergy.shapeshifter.core.service.serialization.UftpSerializer;
3838
import org.lfenergy.shapeshifter.core.service.validation.UftpValidationService;
3939
import org.lfenergy.shapeshifter.core.service.validation.model.ValidationResult;
40+
import org.mockito.ArgumentCaptor;
4041
import org.mockito.Mock;
4142
import org.mockito.junit.jupiter.MockitoExtension;
4243

44+
import java.io.IOException;
45+
import java.net.http.HttpClient;
46+
import java.net.http.HttpRequest;
47+
import java.net.http.HttpResponse;
48+
import java.time.Duration;
4349
import java.util.Locale;
4450

4551
@ExtendWith(MockitoExtension.class)
@@ -86,6 +92,11 @@ class UftpSendMessageServiceTest {
8692
@Mock
8793
private SignedMessage signedMessage;
8894

95+
@Mock
96+
private HttpClient httpClient;
97+
@Mock
98+
private HttpResponse<String> httpResponse;
99+
89100
@BeforeAll
90101
public static void setupWireMockServer() {
91102
wireMockServer = new WireMockServer(0);
@@ -413,6 +424,50 @@ void attemptToValidateAndSendMessage_OutgoingResponseMessageShouldNotBeValidated
413424
verifyNoValidations();
414425
}
415426

427+
@Test
428+
void attemptToSendMessage_appliesReadTimeoutToRequest() throws IOException, InterruptedException {
429+
mockSerialisation();
430+
mockSending();
431+
mockParticipantServiceWithoutAuthorization(getEndpointURL(PATH_HAPPY_FLOW));
432+
433+
var readTimeout = Duration.ofSeconds(42);
434+
435+
testSubject = new UftpSendMessageService(serializer, cryptoService, participantService, authorizationProvider, uftpValidationService, httpClient);
436+
437+
testSubject.addRequestInterceptor(request -> request.timeout(readTimeout));
438+
439+
given(httpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).willReturn(httpResponse);
440+
given(httpResponse.statusCode()).willReturn(200);
441+
442+
testSubject.attemptToSendMessage(flexRequest, details);
443+
444+
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
445+
verify(httpClient).send(requestCaptor.capture(), any());
446+
447+
HttpRequest capturedRequest = requestCaptor.getValue();
448+
assertThat(capturedRequest.timeout()).isPresent().contains(readTimeout);
449+
}
450+
451+
@Test
452+
void attemptToSendMessage_noTimeoutByDefault() throws IOException, InterruptedException {
453+
mockSerialisation();
454+
mockSending();
455+
mockParticipantServiceWithoutAuthorization(getEndpointURL(PATH_HAPPY_FLOW));
456+
457+
testSubject = new UftpSendMessageService(serializer, cryptoService, participantService, authorizationProvider, uftpValidationService, httpClient);
458+
459+
given(httpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).willReturn(httpResponse);
460+
given(httpResponse.statusCode()).willReturn(200);
461+
462+
testSubject.attemptToSendMessage(flexRequest, details);
463+
464+
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
465+
verify(httpClient).send(requestCaptor.capture(), any());
466+
467+
assertThat(requestCaptor.getValue().timeout()).isEmpty();
468+
}
469+
470+
416471
private void mockParticipantServiceWithAuthorization(String endpointUrl) {
417472
UftpParticipantInformation recipientInformation = uftpParticipantInformationBuilder.withEndpoint(endpointUrl).withRequiresAuthorization(true).build();
418473
given(participantService.getParticipantInformation(any(UftpParticipant.class))).willReturn(recipientInformation);

spring/src/main/java/org/lfenergy/shapeshifter/spring/config/ShapeshifterConfiguration.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import javax.net.ssl.SSLContext;
4343
import java.net.http.HttpClient;
4444
import java.util.HashSet;
45+
import java.util.Optional;
4546
import java.util.Set;
4647

4748
@Configuration
@@ -66,7 +67,14 @@ public UftpSendMessageService uftpSendMessageService(UftpSerializer serializer,
6667
ParticipantResolutionService participantService,
6768
ParticipantAuthorizationProvider participantAuthorizationProvider,
6869
UftpValidationService uftpValidationService) {
69-
return new UftpSendMessageService(serializer, cryptoService, participantService, participantAuthorizationProvider, uftpValidationService, httpClient());
70+
71+
var service = new UftpSendMessageService(serializer, cryptoService, participantService, participantAuthorizationProvider, uftpValidationService, httpClient());
72+
73+
Optional.ofNullable(properties.http())
74+
.map(ShapeshifterProperties.HttpProperties::readTimeout)
75+
.ifPresent(timeout -> service.addRequestInterceptor(rb -> rb.timeout(timeout)));
76+
77+
return service;
7078
}
7179

7280
@ConditionalOnMissingBean
@@ -221,6 +229,14 @@ private HttpClient httpClient() {
221229
builder.sslContext(SSLContextFactory.createSSLContext(properties.tls()));
222230
}
223231

232+
if (properties.http() != null) {
233+
log.info("Detected HTTP configuration");
234+
235+
if (properties.http().connectTimeout() != null) {
236+
builder.connectTimeout(properties.http().connectTimeout());
237+
}
238+
}
239+
224240
return builder.build();
225241
}
226242

spring/src/main/java/org/lfenergy/shapeshifter/spring/config/ShapeshifterProperties.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@
88
import org.springframework.boot.context.properties.ConfigurationProperties;
99
import org.springframework.core.io.Resource;
1010

11+
import java.time.Duration;
12+
1113
@ConfigurationProperties(prefix = "shapeshifter")
1214
public record ShapeshifterProperties(
1315
ValidationProperties validation,
14-
TlsProperties tls
16+
TlsProperties tls,
17+
HttpProperties http
1518
) {
1619
public record ValidationProperties(
1720
boolean enabled
@@ -31,4 +34,9 @@ public record TlsProperties(
3134
public static final String DEFAULT_TLS_VERSION = "TLSv1.3";
3235

3336
}
37+
38+
public record HttpProperties(
39+
Duration connectTimeout,
40+
Duration readTimeout
41+
) { }
3442
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
2+
package org.lfenergy.shapeshifter.spring.config;
3+
4+
import org.junit.jupiter.api.Nested;
5+
import org.junit.jupiter.api.Test;
6+
import org.lfenergy.shapeshifter.core.service.ParticipantAuthorizationProvider;
7+
import org.lfenergy.shapeshifter.core.service.UftpErrorProcessor;
8+
import org.lfenergy.shapeshifter.core.service.crypto.UftpCryptoService;
9+
import org.lfenergy.shapeshifter.core.service.participant.ParticipantResolutionService;
10+
import org.lfenergy.shapeshifter.core.service.sending.RequestInterceptor;
11+
import org.lfenergy.shapeshifter.core.service.sending.UftpSendMessageService;
12+
import org.lfenergy.shapeshifter.core.service.serialization.UftpSerializer;
13+
import org.lfenergy.shapeshifter.core.service.validation.UftpMessageSupport;
14+
import org.lfenergy.shapeshifter.core.service.validation.UftpValidationService;
15+
import org.springframework.beans.factory.annotation.Autowired;
16+
import org.springframework.boot.test.context.SpringBootTest;
17+
import org.springframework.test.context.TestPropertySource;
18+
import org.springframework.test.context.bean.override.mockito.MockitoBean;
19+
20+
import java.net.URI;
21+
import java.net.http.HttpClient;
22+
import java.net.http.HttpRequest;
23+
import java.time.Duration;
24+
import java.util.List;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
28+
29+
@SpringBootTest(classes = ShapeshifterConfiguration.class, webEnvironment = SpringBootTest.WebEnvironment.MOCK)
30+
class ShapeshifterConfigurationTest {
31+
32+
@MockitoBean UftpSerializer uftpSerializer;
33+
@MockitoBean UftpCryptoService uftpCryptoService;
34+
@MockitoBean ParticipantResolutionService participantResolutionService;
35+
@MockitoBean ParticipantAuthorizationProvider participantAuthorizationProvider;
36+
@MockitoBean UftpValidationService uftpValidationService;
37+
@MockitoBean UftpMessageSupport uftpMessageSupport;
38+
@MockitoBean UftpErrorProcessor uftpErrorProcessor;
39+
40+
41+
@Nested
42+
@TestPropertySource(properties = "shapeshifter.http.connect-timeout=5s")
43+
class ConnectTimeoutPropertiesSet {
44+
45+
@Autowired
46+
private ShapeshifterProperties properties;
47+
48+
@Test
49+
void shouldConfigureConnectTimeout() throws Exception {
50+
var service = new ShapeshifterConfiguration(properties).uftpSendMessageService(
51+
uftpSerializer, uftpCryptoService, participantResolutionService,
52+
participantAuthorizationProvider, uftpValidationService);
53+
54+
var httpClientField = UftpSendMessageService.class.getDeclaredField("httpClient");
55+
httpClientField.setAccessible(true);
56+
var httpClient = (HttpClient) httpClientField.get(service);
57+
58+
assertThat(httpClient.connectTimeout()).isPresent().contains(Duration.ofSeconds(5));
59+
}
60+
}
61+
62+
@Nested
63+
@TestPropertySource(properties = {"shapeshifter.http.read-timeout=10s"})
64+
class ReadTimeoutPropertiesSet {
65+
66+
@Autowired
67+
private ShapeshifterProperties properties;
68+
69+
@Test
70+
void shouldRegisterReadTimeoutInterceptor() throws Exception {
71+
var service = new ShapeshifterConfiguration(properties).uftpSendMessageService(
72+
uftpSerializer, uftpCryptoService, participantResolutionService,
73+
participantAuthorizationProvider, uftpValidationService);
74+
75+
var requestBuilder = HttpRequest.newBuilder().uri(new URI("http://localhost"));
76+
var interceptorsField = UftpSendMessageService.class.getDeclaredField("requestInterceptors");
77+
interceptorsField.setAccessible(true);
78+
var interceptors = (List<RequestInterceptor>) interceptorsField.get(service);
79+
interceptors.forEach(i -> i.accept(requestBuilder));
80+
81+
assertThat(requestBuilder.build().timeout()).isPresent().contains(Duration.ofSeconds(10));
82+
}
83+
}
84+
85+
86+
@Nested
87+
class NoPropertiesSet {
88+
89+
@Autowired
90+
private ShapeshifterProperties properties;
91+
92+
@Test
93+
void shouldNotConfigureConnectTimeoutByDefault() throws Exception {
94+
var service = new ShapeshifterConfiguration(properties).uftpSendMessageService(
95+
uftpSerializer, uftpCryptoService, participantResolutionService,
96+
participantAuthorizationProvider, uftpValidationService);
97+
98+
var httpClientField = UftpSendMessageService.class.getDeclaredField("httpClient");
99+
httpClientField.setAccessible(true);
100+
var httpClient = (HttpClient) httpClientField.get(service);
101+
102+
assertThat(httpClient.connectTimeout()).isEmpty();
103+
}
104+
105+
@Test
106+
void shouldNotRegisterReadTimeoutInterceptorByDefault() throws Exception {
107+
var service = new ShapeshifterConfiguration(properties).uftpSendMessageService(
108+
uftpSerializer, uftpCryptoService, participantResolutionService,
109+
participantAuthorizationProvider, uftpValidationService);
110+
111+
var interceptorsField = UftpSendMessageService.class.getDeclaredField("requestInterceptors");
112+
interceptorsField.setAccessible(true);
113+
var interceptors = (List<?>) interceptorsField.get(service);
114+
115+
assertThat(interceptors).isEmpty();
116+
}
117+
}
118+
119+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package org.lfenergy.shapeshifter.spring.config;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
5+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
6+
7+
import java.time.Duration;
8+
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
11+
class ShapeshifterPropertiesTest {
12+
13+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
14+
.withUserConfiguration(TestConfig.class);
15+
16+
@EnableConfigurationProperties(ShapeshifterProperties.class)
17+
static class TestConfig {
18+
}
19+
20+
@Test
21+
void shouldBindHttpProperties() {
22+
contextRunner.withPropertyValues(
23+
"shapeshifter.http.connect-timeout=5s",
24+
"shapeshifter.http.read-timeout=10s"
25+
).run(context -> {
26+
assertThat(context).hasSingleBean(ShapeshifterProperties.class);
27+
var properties = context.getBean(ShapeshifterProperties.class);
28+
29+
assertThat(properties.http()).isNotNull();
30+
assertThat(properties.http().connectTimeout()).isEqualTo(Duration.ofSeconds(5));
31+
assertThat(properties.http().readTimeout()).isEqualTo(Duration.ofSeconds(10));
32+
});
33+
}
34+
35+
@Test
36+
void shouldHandleMissingHttpProperties() {
37+
contextRunner.run(context -> {
38+
assertThat(context).hasSingleBean(ShapeshifterProperties.class);
39+
var properties = context.getBean(ShapeshifterProperties.class);
40+
41+
assertThat(properties.http()).isNull();
42+
});
43+
}
44+
}

0 commit comments

Comments
 (0)