Skip to content

Commit 6768c03

Browse files
authored
Merge pull request #300 from digipost/exception-on-not-closing-stream
Exception on not closing ResponseInputStream
2 parents 4047505 + d8902bf commit 6768c03

File tree

10 files changed

+180
-31
lines changed

10 files changed

+180
-31
lines changed

NOTICE

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,14 @@ Licensed under Apache 2 - http://www.apache.org/licenses/LICENSE-2.0.html
88

99
This software includes third party software subject to the following licenses:
1010

11-
Apache Commons Codec under Apache License, Version 2.0
1211
Apache HttpClient under Apache License, Version 2.0
1312
Apache HttpComponents Core HTTP/1.1 under Apache License, Version 2.0
1413
Apache HttpComponents Core HTTP/2 under Apache License, Version 2.0
15-
Apache HttpCore under Apache License, Version 2.0
1614
Digipost Certificate Validator under The Apache Software License, Version 2.0
1715
Digipost JAXB Resolver - com.sun.xml.bind under The Apache Software License, Version 2.0
1816
JavaBeans Activation Framework API jar under CDDL/GPLv2+CE
1917
jaxb-api under CDDL 1.1 or GPL2 w/ CPE
2018
JAXB2 Basics - Runtime under BSD-Style License
21-
JCL 1.2 implemented over SLF4J under Apache License, Version 2.0
2219
Old JAXB Core under CDDL+GPL License
2320
Old JAXB Runtime under Eclipse Distribution License - v 1.0
2421
Posten signering - API JAXB Classes under The Apache Software License, Version 2.0

lib/NOTICE

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,14 @@ Licensed under Apache 2 - http://www.apache.org/licenses/LICENSE-2.0.html
88

99
This software includes third party software subject to the following licenses:
1010

11-
Apache Commons Codec under Apache License, Version 2.0
1211
Apache HttpClient under Apache License, Version 2.0
1312
Apache HttpComponents Core HTTP/1.1 under Apache License, Version 2.0
1413
Apache HttpComponents Core HTTP/2 under Apache License, Version 2.0
15-
Apache HttpCore under Apache License, Version 2.0
1614
Digipost Certificate Validator under The Apache Software License, Version 2.0
1715
Digipost JAXB Resolver - com.sun.xml.bind under The Apache Software License, Version 2.0
1816
JavaBeans Activation Framework API jar under CDDL/GPLv2+CE
1917
jaxb-api under CDDL 1.1 or GPL2 w/ CPE
2018
JAXB2 Basics - Runtime under BSD-Style License
21-
JCL 1.2 implemented over SLF4J under Apache License, Version 2.0
2219
Old JAXB Core under CDDL+GPL License
2320
Old JAXB Runtime under Eclipse Distribution License - v 1.0
2421
Posten signering - API JAXB Classes under The Apache Software License, Version 2.0

lib/pom.xml

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
<dependency>
2929
<groupId>org.junit</groupId>
3030
<artifactId>junit-bom</artifactId>
31-
<version>5.9.2</version>
31+
<version>5.10.0</version>
3232
<type>pom</type>
3333
<scope>import</scope>
3434
</dependency>
@@ -55,7 +55,7 @@
5555
<dependency>
5656
<groupId>no.digipost</groupId>
5757
<artifactId>certificate-validator</artifactId>
58-
<version>2.6</version>
58+
<version>3.0.2</version>
5959
<exclusions>
6060
<exclusion>
6161
<groupId>org.bouncycastle</groupId>
@@ -72,26 +72,26 @@
7272
<dependency>
7373
<groupId>org.apache.httpcomponents.core5</groupId>
7474
<artifactId>httpcore5</artifactId>
75-
<version>5.2.1</version>
75+
<version>5.2.2</version>
7676
</dependency>
7777

7878
<dependency>
7979
<groupId>commons-io</groupId>
8080
<artifactId>commons-io</artifactId>
81-
<version>2.11.0</version>
81+
<version>2.13.0</version>
8282
<scope>test</scope>
8383
</dependency>
8484

8585
<dependency>
8686
<groupId>org.slf4j</groupId>
8787
<artifactId>slf4j-api</artifactId>
88-
<version>2.0.6</version>
88+
<version>2.0.9</version>
8989
<scope>test</scope>
9090
</dependency>
9191
<dependency>
9292
<groupId>org.slf4j</groupId>
9393
<artifactId>slf4j-simple</artifactId>
94-
<version>2.0.6</version>
94+
<version>2.0.9</version>
9595
<scope>test</scope>
9696
</dependency>
9797

@@ -124,7 +124,7 @@
124124
<dependency>
125125
<groupId>nl.jqno.equalsverifier</groupId>
126126
<artifactId>equalsverifier</artifactId>
127-
<version>3.14</version>
127+
<version>3.15.1</version>
128128
<scope>test</scope>
129129
</dependency>
130130
<dependency>
@@ -155,7 +155,7 @@
155155
<dependency>
156156
<groupId>com.github.tomakehurst</groupId>
157157
<artifactId>wiremock-jre8</artifactId>
158-
<version>2.35.0</version>
158+
<version>2.35.1</version>
159159
<scope>test</scope>
160160
<exclusions>
161161
<exclusion>
@@ -188,7 +188,7 @@
188188
<plugin>
189189
<groupId>com.github.siom79.japicmp</groupId>
190190
<artifactId>japicmp-maven-plugin</artifactId>
191-
<version>0.15.3</version>
191+
<version>0.17.3</version>
192192
<configuration>
193193
<parameter>
194194
<includes>
@@ -202,7 +202,7 @@
202202
</plugin>
203203
<plugin>
204204
<artifactId>maven-shade-plugin</artifactId>
205-
<version>3.4.1</version>
205+
<version>3.5.0</version>
206206
<configuration>
207207
<minimizeJar>true</minimizeJar>
208208
<artifactSet>
@@ -228,11 +228,11 @@
228228
</plugin>
229229
<plugin>
230230
<artifactId>maven-surefire-plugin</artifactId>
231-
<version>3.0.0</version>
231+
<version>3.1.2</version>
232232
</plugin>
233233
<plugin>
234234
<artifactId>maven-resources-plugin</artifactId>
235-
<version>3.3.0</version>
235+
<version>3.3.1</version>
236236
</plugin>
237237
<plugin>
238238
<artifactId>maven-javadoc-plugin</artifactId>
@@ -244,7 +244,7 @@
244244
</plugin>
245245
<plugin>
246246
<artifactId>maven-enforcer-plugin</artifactId>
247-
<version>3.2.1</version>
247+
<version>3.4.1</version>
248248
<configuration>
249249
<rules>
250250
<bannedDependencies>

lib/src/main/java/no/digipost/signature/client/ClientConfiguration.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,29 @@ public Builder timeoutsForDocumentDownloads(Consumer<? super TimeoutsConfigurer>
238238
}
239239

240240

241+
/**
242+
* Configure the pool used by the client to manage connections
243+
* used for integrating with the API.
244+
*
245+
* @param connectionPool the {@link ConnectionPoolConfigurer connection pool configuration API}
246+
*/
247+
public Builder connectionPool(Consumer<? super ConnectionPoolConfigurer> connectionPool) {
248+
connectionPool.accept(defaultHttpClientConfigurer);
249+
return this;
250+
}
251+
252+
253+
/**
254+
* Configure the pool used by the client to manage connections
255+
* used specifically for downloading documents.
256+
*
257+
* @param connectionPool the {@link ConnectionPoolConfigurer connection pool configuration API}
258+
*/
259+
public Builder connectionPoolForDocumentDownloads(Consumer<? super ConnectionPoolConfigurer> connectionPool) {
260+
connectionPool.accept(httpClientForDocumentDownloadsConfigurer);
261+
return this;
262+
}
263+
241264
/**
242265
* This method allows for custom configuration of the created {@link HttpClient} if anything is
243266
* needed that is not already supported by other methods in {@link ClientConfiguration}.
@@ -333,7 +356,8 @@ public Builder serverCertificateTrustStrategy(CertificateChainValidation certifi
333356
* Allows for overriding which {@link Clock} is used to convert between Java and XML,
334357
* may be useful for e.g. automated tests.
335358
* <p>
336-
* Uses {@link Clock#systemDefaultZone() the best available system clock} if not specified.
359+
* Uses the {@link Clock#systemDefaultZone() system clock with default time zone}
360+
* if not specified.
337361
*/
338362
public Builder clock(Clock clock) {
339363
this.clock = clock;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package no.digipost.signature.client;
2+
3+
/**
4+
* Allows configuring aspects of the connection pool
5+
* used by the internal HTTP client.
6+
*/
7+
public interface ConnectionPoolConfigurer {
8+
9+
/**
10+
* The default amount of connections in the connection pool is
11+
* {@value #DEFAULT_TOTAL_CONNECTIONS_IN_POOL}.
12+
* This may be overridden using {@link #maxTotalConnectionsInPool(int)}
13+
*/
14+
int DEFAULT_TOTAL_CONNECTIONS_IN_POOL = 10;
15+
16+
17+
/**
18+
* Set the amount of connections in the connection pool, if the default
19+
* of {@value #DEFAULT_TOTAL_CONNECTIONS_IN_POOL} is not applicable for
20+
* your integration.
21+
*
22+
* @param count the amount of connections in the connection pool
23+
*/
24+
ConnectionPoolConfigurer maxTotalConnectionsInPool(int count);
25+
26+
}

lib/src/main/java/no/digipost/signature/client/core/exceptions/HttpIOException.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@ public HttpIOException(HttpResponse response, IOException cause) {
1616
}
1717

1818
public HttpIOException(HttpRequest request, IOException cause) {
19+
this(request, null, cause);
20+
}
21+
22+
public HttpIOException(HttpRequest request, String message, IOException cause) {
1923
this(
2024
"Error executing " + request + ", because " +
21-
cause.getClass().getSimpleName() + " '" + cause.getMessage() + "'",
25+
cause.getClass().getSimpleName() + " '" + cause.getMessage() + "'" +
26+
(message != null ? ". " + message : ""),
2227
cause);
2328
}
2429

lib/src/main/java/no/digipost/signature/client/core/internal/DownloadHelper.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.apache.hc.client5.http.classic.HttpClient;
88
import org.apache.hc.core5.http.ClassicHttpRequest;
99
import org.apache.hc.core5.http.ClassicHttpResponse;
10+
import org.apache.hc.core5.http.ConnectionRequestTimeoutException;
1011
import org.apache.hc.core5.http.ContentType;
1112
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
1213
import org.apache.hc.core5.http.message.BasicHeader;
@@ -53,6 +54,16 @@ public ResponseInputStream getDataStream(URI absoluteUri, ContentType ... accept
5354
try {
5455
try {
5556
response = httpClient.executeOpen(null, request, null);
57+
} catch (ConnectionRequestTimeoutException connectionRequestTimeoutException) {
58+
throw new HttpIOException(request,
59+
"This happens when an HTTP connection could not be obtained from the connection pool, and " +
60+
"is likely because of missing resource management of responses when downloading data, " +
61+
"e.g. signed documents (PAdESes). Please verify your implemention guarantees that InputStreams " +
62+
"obtained from the client library are properly handled and closed, e.g. using try-with-resources " +
63+
"in Java or .use { .. } in Kotlin. This attention is only necessary for data streams, and not " +
64+
"for methods yielding regular Java objects, where parsing and closing the API responses is " +
65+
"the internal responsibility of the client library.",
66+
connectionRequestTimeoutException);
5667
} catch (IOException e) {
5768
throw new HttpIOException(request, e);
5869
}

lib/src/main/java/no/digipost/signature/client/core/internal/configuration/ApacheHttpClientBuilderConfigurer.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package no.digipost.signature.client.core.internal.configuration;
22

33
import no.digipost.signature.client.ApacheHttpClientConfigurer;
4+
import no.digipost.signature.client.ConnectionPoolConfigurer;
45
import no.digipost.signature.client.TimeoutsConfigurer;
56
import org.apache.hc.client5.http.config.ConnectionConfig;
67
import org.apache.hc.client5.http.config.RequestConfig;
@@ -11,9 +12,10 @@
1112
import java.time.Duration;
1213
import java.util.function.Consumer;
1314

14-
public final class ApacheHttpClientBuilderConfigurer implements TimeoutsConfigurer, ApacheHttpClientConfigurer, Configurer<HttpClientBuilder> {
15+
public final class ApacheHttpClientBuilderConfigurer implements TimeoutsConfigurer, ConnectionPoolConfigurer, ApacheHttpClientConfigurer, Configurer<HttpClientBuilder> {
1516

1617
private final PoolingHttpClientConnectionManagerBuilder connectionManagerConfig = PoolingHttpClientConnectionManagerBuilder.create();
18+
1719
private final SocketConfig.Builder socketConfig = SocketConfig.custom();
1820
private final ConnectionConfig.Builder connectionConfig = ConnectionConfig.custom();
1921
private final RequestConfig.Builder requestConfig = RequestConfig.custom();
@@ -26,12 +28,16 @@ public final class ApacheHttpClientBuilderConfigurer implements TimeoutsConfigur
2628
.setDefaultRequestConfig(requestConfig.build())
2729
.setConnectionManager(connectionManagerConfig.build());
2830

29-
private Configurer<? super PoolingHttpClientConnectionManagerBuilder> additionalConnectionManagerConfigurer = Configurer.notConfigured();
30-
private Configurer<? super HttpClientBuilder> additionalHttpClientConfigurer = Configurer.notConfigured();
31+
private Configurer<PoolingHttpClientConnectionManagerBuilder> additionalConnectionManagerConfigurer = Configurer.notConfigured();
32+
private Configurer<HttpClientBuilder> additionalHttpClientConfigurer = Configurer.notConfigured();
33+
3134

35+
public ApacheHttpClientBuilderConfigurer() {
36+
maxTotalConnectionsInPool(DEFAULT_TOTAL_CONNECTIONS_IN_POOL);
37+
}
3238

3339
public ApacheHttpClientBuilderConfigurer connectionManager(Configurer<PoolingHttpClientConnectionManagerBuilder> connectionManagerConfigurer) {
34-
this.additionalConnectionManagerConfigurer = connectionManagerConfigurer;
40+
this.additionalConnectionManagerConfigurer = additionalConnectionManagerConfigurer.andThen(connectionManagerConfigurer);
3541
return this;
3642
}
3743

@@ -59,6 +65,16 @@ public ApacheHttpClientBuilderConfigurer responseArrivalTimeout(Duration duratio
5965
return this;
6066
}
6167

68+
@Override
69+
public ApacheHttpClientBuilderConfigurer maxTotalConnectionsInPool(int count) {
70+
return connectionManager(connectionMgr -> connectionMgr
71+
72+
// a "route" is essentially the target host (optionally with proxy hops). The HTTP client
73+
// is specifically for one API, and separating connections per route is not applicable,
74+
// therefore the per route count is the same as total count of connections
75+
.setMaxConnPerRoute(count).setMaxConnTotal(count));
76+
}
77+
6278
@Override
6379
public ApacheHttpClientBuilderConfigurer configure(
6480
Consumer<? super SocketConfig.Builder> socketConfig,
@@ -73,7 +89,7 @@ public ApacheHttpClientBuilderConfigurer configure(
7389

7490
@Override
7591
public ApacheHttpClientBuilderConfigurer configure(Consumer<? super HttpClientBuilder> httpClientCustomizer) {
76-
this.additionalHttpClientConfigurer = Configurer.of(httpClientCustomizer);
92+
this.additionalHttpClientConfigurer = additionalHttpClientConfigurer.andThen(Configurer.of(httpClientCustomizer));
7793
return this;
7894
}
7995

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package no.digipost.signature.client.portal;
2+
3+
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
4+
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
5+
import no.digipost.signature.client.ClientConfiguration;
6+
import no.digipost.signature.client.ServiceEnvironment;
7+
import no.digipost.signature.client.core.PAdESReference;
8+
import no.digipost.signature.client.core.Sender;
9+
import no.digipost.signature.client.core.exceptions.HttpIOException;
10+
import org.apache.hc.core5.http.ConnectionRequestTimeoutException;
11+
import org.junit.jupiter.api.Test;
12+
import org.junit.jupiter.api.Timeout;
13+
14+
import java.net.URI;
15+
import java.util.stream.Stream;
16+
17+
import static com.github.tomakehurst.wiremock.client.WireMock.get;
18+
import static com.github.tomakehurst.wiremock.client.WireMock.givenThat;
19+
import static com.github.tomakehurst.wiremock.client.WireMock.ok;
20+
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
21+
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
22+
import static com.github.tomakehurst.wiremock.http.RequestMethod.GET;
23+
import static com.github.tomakehurst.wiremock.matching.RequestPatternBuilder.newRequestPattern;
24+
import static com.github.tomakehurst.wiremock.matching.UrlPattern.ANY;
25+
import static java.time.Duration.ofMillis;
26+
import static java.util.concurrent.TimeUnit.SECONDS;
27+
import static no.digipost.signature.client.ServiceEnvironment.STAGING;
28+
import static no.digipost.signature.client.TestKonfigurasjon.CLIENT_KEYSTORE;
29+
import static org.hamcrest.MatcherAssert.assertThat;
30+
import static org.hamcrest.Matchers.containsStringIgnoringCase;
31+
import static org.hamcrest.Matchers.instanceOf;
32+
import static org.junit.jupiter.api.Assertions.assertAll;
33+
import static org.junit.jupiter.api.Assertions.assertThrows;
34+
import static uk.co.probablyfine.matchers.Java8Matchers.where;
35+
36+
@WireMockTest
37+
class PortalClientConnectionPoolTest {
38+
39+
private final ServiceEnvironment unitTestEnv;
40+
private final ClientConfiguration.Builder configBuilder;
41+
42+
PortalClientConnectionPoolTest(WireMockRuntimeInfo wireMockInfo) {
43+
this.unitTestEnv = STAGING.withServiceUrl(URI.create(wireMockInfo.getHttpBaseUrl()));
44+
this.configBuilder = ClientConfiguration.builder(CLIENT_KEYSTORE)
45+
.serviceEnvironment(unitTestEnv)
46+
.defaultSender(new Sender("123456789"));
47+
}
48+
49+
@Test
50+
@Timeout(value = 3, unit = SECONDS)
51+
void exhaustingConnectionPoolWhenMissingResourceManagement() {
52+
53+
int connectionPoolSize = 15;
54+
PortalClient client = new PortalClient(configBuilder
55+
.timeoutsForDocumentDownloads(t -> t.connectionRequestTimeout(ofMillis(100)))
56+
.connectionPoolForDocumentDownloads(pool -> pool.maxTotalConnectionsInPool(connectionPoolSize))
57+
.build());
58+
59+
givenThat(get(ANY).willReturn(ok()));
60+
61+
PAdESReference padesReference = PAdESReference.of(URI.create(unitTestEnv.signatureServiceRootUrl() + "/pades"));
62+
HttpIOException thrown = assertThrows(HttpIOException.class, () -> Stream.generate(() -> padesReference).forEach(client::getPAdES));
63+
64+
assertAll(
65+
() -> assertThat(thrown, where(Throwable::getCause, instanceOf(ConnectionRequestTimeoutException.class))),
66+
() -> assertThat(thrown, where(Throwable::getMessage, containsStringIgnoringCase("could not be obtained from the connection pool"))),
67+
() -> assertThat(thrown, where(Throwable::getMessage, containsStringIgnoringCase("missing resource management of responses")))
68+
);
69+
70+
verify(connectionPoolSize, newRequestPattern(GET, urlPathMatching(".*/pades$")));
71+
}
72+
73+
}

0 commit comments

Comments
 (0)