Skip to content

Commit 2b9895e

Browse files
authored
Merge pull request cloudant#508 from cloudant/507-cookie-response-stream
Closed successful session response body stream
2 parents 7299022 + 93269f1 commit 2b9895e

File tree

3 files changed

+71
-16
lines changed

3 files changed

+71
-16
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Unreleased
2+
- [FIXED] Connection leak regression introduced in 2.18.0 caused by not closing streams from
3+
successful session response bodies.
4+
15
# 2.19.0 (2020-03-02)
26
- [NEW] Add getter for total row count to `AllDocsResponse`
37
- [NEW] Added `DbInfo#getSizes()` for access to improved sizes information.

cloudant-client/src/test/java/com/cloudant/tests/HttpTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
import java.io.IOException;
8282
import java.io.InputStream;
8383
import java.io.InputStreamReader;
84+
import java.net.HttpURLConnection;
8485
import java.net.URI;
8586
import java.net.URL;
8687
import java.net.URLClassLoader;
@@ -96,6 +97,7 @@
9697
import java.util.concurrent.Future;
9798
import java.util.concurrent.TimeUnit;
9899
import java.util.concurrent.atomic.AtomicInteger;
100+
import java.util.concurrent.atomic.AtomicReference;
99101
import java.util.regex.Pattern;
100102
import java.util.stream.Stream;
101103

@@ -1254,5 +1256,48 @@ public static RecordedRequest[] takeN(MockWebServer server, int n) throws Except
12541256
return recordedRequests;
12551257
}
12561258

1259+
/**
1260+
* Test that a response stream from a successful _session request is consumed and closed.
1261+
*
1262+
* @throws Exception
1263+
*/
1264+
@TestTemplate
1265+
public void successfulSessionStreamClose() throws Exception {
1266+
// Queue a mock response for the _session request
1267+
mockWebServer.enqueue(OK_COOKIE);
1268+
// Queue a mock response for an _all_dbs request
1269+
mockWebServer.enqueue(new MockResponse().setBody("[]"));
12571270

1271+
final AtomicReference<InputStream> responseStream = new AtomicReference<InputStream>();
1272+
CloudantClient c = CloudantClientHelper.newMockWebServerClientBuilder(mockWebServer)
1273+
.interceptors(new HttpConnectionResponseInterceptor() {
1274+
@Override
1275+
public HttpConnectionInterceptorContext interceptResponse(HttpConnectionInterceptorContext context) {
1276+
try {
1277+
// Store the response stream from the session request
1278+
if (context.connection.url.toString().contains("_session")) {
1279+
HttpURLConnection conn = context.connection.getConnection();
1280+
responseStream.set(context.connection.getConnection().getInputStream());
1281+
}
1282+
} catch (IOException e) {
1283+
fail("IOException in test interceptor.");
1284+
}
1285+
return context;
1286+
}
1287+
})
1288+
.username("a")
1289+
.password("b")
1290+
.build();
1291+
// Make a request to init the session
1292+
List<String> dbs = c.getAllDbs();
1293+
// Get the response stream from the session request
1294+
InputStream stream = responseStream.get();
1295+
// Assert that the stream has been consumed
1296+
assertEquals(0, stream.available(), "There should be no bytes available from the stream.");
1297+
// Assert that the stream is closed
1298+
assertThrows(IOException.class,
1299+
() -> stream.read(),
1300+
"Reading the stream should throw an IOException because the stream should be " +
1301+
"closed.");
1302+
}
12581303
}

cloudant-http/src/main/java/com/cloudant/http/internal/interceptors/CookieInterceptorBase.java

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ protected CookieInterceptorBase(String baseUrl, String endpoint, String requestM
8787
*/
8888
protected abstract byte[] getSessionRequestPayload(HttpConnectionInterceptorContext context);
8989

90-
private void requestCookie(HttpConnectionInterceptorContext context) {
90+
private void requestCookie(HttpConnectionInterceptorContext context) throws IOException {
9191
// Check if the session was already updated on another thread before getting a cookie
9292
sessionLock.readLock().lock();
9393
try {
@@ -99,7 +99,14 @@ private void requestCookie(HttpConnectionInterceptorContext context) {
9999
if (sessionId.equals(context.getState(this, sessionStateName, UUID.class))) {
100100
HttpConnection sessionConn = makeSessionRequest(sessionRequestUrl,
101101
getSessionRequestPayload(context), sessionRequestMimeType, context);
102-
storeCookiesFromResponse(sessionConn.getConnection());
102+
HttpURLConnection sessionUrlConnection = sessionConn.getConnection();
103+
try {
104+
storeCookiesFromResponse(sessionUrlConnection);
105+
} finally {
106+
// We use collect rather than consume as we don't want to log
107+
// a warning, even though we don't actually need the body
108+
Utils.collectAndCloseStream(sessionUrlConnection.getInputStream());
109+
}
103110
// We renewed a cookie, update the global sessionID and this request's context
104111
sessionId = UUID.randomUUID();
105112
context.setState(this, sessionStateName, sessionId);
@@ -163,21 +170,20 @@ public HttpConnectionInterceptorContext interceptRequest(HttpConnectionIntercept
163170
// Set the sessionId for this request
164171
context.setState(this, sessionStateName, sessionId);
165172
HttpURLConnection connection = context.connection.getConnection();
173+
try {
174+
// First time we will have no cookies
175+
if (cookieManager.getCookieStore().getCookies().isEmpty()) {
176+
requestCookie(context);
177+
}
166178

167-
// First time we will have no cookies
168-
if (cookieManager.getCookieStore().getCookies().isEmpty()) {
169-
requestCookie(context);
170-
}
171-
172-
// Debug logging
173-
if (logger.isLoggable(Level.FINEST)) {
174-
logger.finest("Attempt to add cookie to request.");
175-
logger.finest("Cookies are stored for URIs: " + cookieManager.getCookieStore()
176-
.getURIs());
177-
}
179+
// Debug logging
180+
if (logger.isLoggable(Level.FINEST)) {
181+
logger.finest("Attempt to add cookie to request.");
182+
logger.finest("Cookies are stored for URIs: " + cookieManager.getCookieStore()
183+
.getURIs());
184+
}
178185

179-
// Apply any saved cookies to the request
180-
try {
186+
// Apply any saved cookies to the request
181187
Map<String, List<String>> requestCookieHeaders = cookieManager.get(connection
182188
.getURL().toURI(), connection.getRequestProperties());
183189
for (Map.Entry<String, List<String>> requestCookieHeader :
@@ -217,7 +223,7 @@ public HttpConnectionInterceptorContext interceptResponse(HttpConnectionIntercep
217223
context.replayRequest = true;
218224
}
219225
} catch (IOException e) {
220-
throw wrapIOException("Failed to read HTTP reponse code or body from", connection, e);
226+
throw wrapIOException("Failed to read HTTP response code or body from", connection, e);
221227
}
222228
return context;
223229
}

0 commit comments

Comments
 (0)