Skip to content
This repository was archived by the owner on May 28, 2018. It is now read-only.

Commit c27d329

Browse files
committed
JERSEY-3214 Bugfix to prevent exception when closing unconsumed Response. Affected only httpclient 4.5.1+
1 parent 364e980 commit c27d329

File tree

3 files changed

+235
-153
lines changed

3 files changed

+235
-153
lines changed

Diff for: connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheConnector.java

+55-31
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
import java.io.ByteArrayInputStream;
4545
import java.io.ByteArrayOutputStream;
4646
import java.io.Closeable;
47-
import java.io.FilterInputStream;
4847
import java.io.IOException;
4948
import java.io.InputStream;
5049
import java.io.OutputStream;
@@ -64,7 +63,7 @@
6463
import javax.ws.rs.core.HttpHeaders;
6564
import javax.ws.rs.core.MultivaluedMap;
6665
import javax.ws.rs.core.Response;
67-
66+
import javax.ws.rs.core.Response.StatusType;
6867
import javax.net.ssl.HostnameVerifier;
6968
import javax.net.ssl.SSLContext;
7069
import javax.net.ssl.SSLSocketFactory;
@@ -100,6 +99,7 @@
10099
import org.apache.http.config.ConnectionConfig;
101100
import org.apache.http.config.Registry;
102101
import org.apache.http.config.RegistryBuilder;
102+
import org.apache.http.conn.ConnectionReleaseTrigger;
103103
import org.apache.http.conn.HttpClientConnectionManager;
104104
import org.apache.http.conn.ManagedHttpClientConnection;
105105
import org.apache.http.conn.routing.HttpRoute;
@@ -430,8 +430,8 @@ public ClientResponse apply(final ClientRequest clientRequest) throws Processing
430430
final HttpUriRequest request = getUriHttpRequest(clientRequest);
431431
final Map<String, String> clientHeadersSnapshot = writeOutBoundHeaders(clientRequest.getHeaders(), request);
432432

433+
CloseableHttpResponse response = null;
433434
try {
434-
final CloseableHttpResponse response;
435435
final HttpClientContext context = HttpClientContext.create();
436436
if (preemptiveBasicAuth) {
437437
final AuthCache authCache = new BasicAuthCache();
@@ -442,11 +442,14 @@ public ClientResponse apply(final ClientRequest clientRequest) throws Processing
442442
response = client.execute(getHost(request), request, context);
443443
HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, clientRequest.getHeaders(), this.getClass().getName());
444444

445+
final HttpEntity entity = response.getEntity();
446+
final InputStream entityContent = entity != null ? entity.getContent() : null;
447+
445448
final Response.StatusType status = response.getStatusLine().getReasonPhrase() == null
446449
? Statuses.from(response.getStatusLine().getStatusCode())
447450
: Statuses.from(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase());
448451

449-
final ClientResponse responseContext = new ClientResponse(status, clientRequest);
452+
final ClientResponse responseContext = new ApacheClientResponse(status, clientRequest, response, entityContent);
450453
final List<URI> redirectLocations = context.getRedirectLocations();
451454
if (redirectLocations != null && !redirectLocations.isEmpty()) {
452455
responseContext.setResolvedRequestUri(redirectLocations.get(redirectLocations.size() - 1));
@@ -464,8 +467,6 @@ public ClientResponse apply(final ClientRequest clientRequest) throws Processing
464467
headers.put(headerName, list);
465468
}
466469

467-
final HttpEntity entity = response.getEntity();
468-
469470
if (entity != null) {
470471
if (headers.get(HttpHeaders.CONTENT_LENGTH) == null) {
471472
headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(entity.getContentLength()));
@@ -477,15 +478,18 @@ public ClientResponse apply(final ClientRequest clientRequest) throws Processing
477478
}
478479
}
479480

480-
try {
481-
responseContext.setEntityStream(new HttpClientResponseInputStream(getInputStream(response)));
482-
} catch (final IOException e) {
483-
LOGGER.log(Level.SEVERE, null, e);
484-
}
481+
responseContext.setEntityStream(bufferedStream(entityContent));
482+
483+
// prevent response-close on correct return
484+
response = null;
485485

486486
return responseContext;
487487
} catch (final Exception e) {
488488
throw new ProcessingException(e);
489+
} finally {
490+
if (response != null) {
491+
ReaderWriter.safelyClose(response);
492+
}
489493
}
490494
}
491495

@@ -617,40 +621,60 @@ private static Map<String, String> writeOutBoundHeaders(final MultivaluedMap<Str
617621
return stringHeaders;
618622
}
619623

620-
private static final class HttpClientResponseInputStream extends FilterInputStream {
624+
/**
625+
* Overrides Response-close() to release the connection without consuming it.
626+
*
627+
* From <a href="http://hc.apache.org/httpcomponents-client-4.5.x/tutorial/html/fundamentals.html#d5e145">Apache HttpClient
628+
* documentation</a>:
629+
*
630+
* <q>The difference between closing the content stream and closing the response is that the former will attempt to keep
631+
* the underlying connection alive by consuming the entity content while the latter immediately shuts down and discards
632+
* the connection.</q>
633+
*
634+
* JAX-RS spec is silent whether closing the content stream consumes the response or closes the connection. This
635+
* ApacheConnector follows apache-behaviour.
636+
*/
637+
private static final class ApacheClientResponse extends ClientResponse {
638+
639+
private final CloseableHttpResponse httpResponse;
621640

622-
HttpClientResponseInputStream(final InputStream inputStream) throws IOException {
623-
super(inputStream);
641+
private final InputStream entityContent;
642+
643+
public ApacheClientResponse(StatusType status, ClientRequest requestContext, CloseableHttpResponse httpResponse,
644+
InputStream entityContent) {
645+
super(status, requestContext);
646+
this.httpResponse = httpResponse;
647+
this.entityContent = entityContent;
624648
}
625649

626650
@Override
627-
public void close() throws IOException {
628-
super.close();
651+
public void close() {
652+
try {
653+
if (entityContent instanceof ConnectionReleaseTrigger) {
654+
// necessary to prevent an exception during stream-close in apache httpclient 4.5.1+
655+
((ConnectionReleaseTrigger) entityContent).abortConnection();
656+
}
657+
httpResponse.close();
658+
} catch (IOException e) {
659+
// Cannot happen according to ConnectionHolder#releaseConnection
660+
throw new ProcessingException(e);
661+
} finally {
662+
super.close();
663+
}
629664
}
630665
}
631666

632-
private static InputStream getInputStream(final CloseableHttpResponse response) throws IOException {
667+
private static InputStream bufferedStream(final InputStream entityContent) {
633668

634669
final InputStream inputStream;
635670

636-
if (response.getEntity() == null) {
671+
if (entityContent == null) {
637672
inputStream = new ByteArrayInputStream(new byte[0]);
638673
} else {
639-
final InputStream i = response.getEntity().getContent();
640-
if (i.markSupported()) {
641-
inputStream = i;
642-
} else {
643-
inputStream = new BufferedInputStream(i, ReaderWriter.BUFFER_SIZE);
644-
}
674+
inputStream = new BufferedInputStream(entityContent, ReaderWriter.BUFFER_SIZE);
645675
}
646676

647-
return new FilterInputStream(inputStream) {
648-
@Override
649-
public void close() throws IOException {
650-
response.close();
651-
super.close();
652-
}
653-
};
677+
return inputStream;
654678
}
655679

656680
private static class ConnectionFactory extends ManagedHttpClientConnectionFactory {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package org.glassfish.jersey.apache.connector;
2+
3+
import static org.junit.Assert.assertEquals;
4+
5+
import java.io.IOException;
6+
import java.io.InputStream;
7+
import java.util.concurrent.TimeUnit;
8+
9+
import javax.ws.rs.GET;
10+
import javax.ws.rs.Path;
11+
import javax.ws.rs.Produces;
12+
import javax.ws.rs.core.Application;
13+
import javax.ws.rs.core.MediaType;
14+
import javax.ws.rs.core.Response;
15+
16+
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
17+
import org.glassfish.jersey.client.ClientConfig;
18+
import org.glassfish.jersey.server.ResourceConfig;
19+
import org.glassfish.jersey.test.JerseyTest;
20+
import org.junit.Test;
21+
22+
public class ClosingTest extends JerseyTest {
23+
24+
private PoolingHttpClientConnectionManager connectionManager;
25+
26+
@Override
27+
protected Application configure() {
28+
ResourceConfig config = new ResourceConfig();
29+
config.register(TestResource.class);
30+
return config;
31+
}
32+
33+
@Override
34+
protected void configureClient(ClientConfig config) {
35+
this.connectionManager = new PoolingHttpClientConnectionManager(60, TimeUnit.SECONDS);
36+
config.property(ApacheClientProperties.CONNECTION_MANAGER, connectionManager);
37+
config.connectorProvider(new ApacheConnectorProvider());
38+
}
39+
40+
private int getLeasedConnections() {
41+
return connectionManager.getTotalStats().getLeased();
42+
}
43+
44+
private int getAvailableConnections() {
45+
return connectionManager.getTotalStats().getAvailable();
46+
}
47+
48+
private int getOpenConnections() {
49+
return getLeasedConnections() + getAvailableConnections();
50+
}
51+
52+
@Test
53+
public void testClosingUnconsumedResponseAbortsConnection() throws Exception {
54+
assertEquals(0, getOpenConnections());
55+
56+
Response response = target().path("productInfo")
57+
.request(MediaType.TEXT_PLAIN_TYPE)
58+
.get();
59+
assertEquals(200, response.getStatus());
60+
61+
assertEquals(1, getLeasedConnections());
62+
InputStream entityStream = response.readEntity(InputStream.class);
63+
64+
// should close the connection without consuming it. must not throw here
65+
response.close();
66+
assertEquals(0, getOpenConnections());
67+
68+
// must not throw here
69+
entityStream.close();
70+
assertEquals(0, getOpenConnections());
71+
}
72+
73+
@Test
74+
public void testClosingUnconsumedStreamConsumesConnection() throws Exception {
75+
assertEquals(0, getOpenConnections());
76+
77+
Response response = target().path("productInfo")
78+
.request(MediaType.TEXT_PLAIN_TYPE)
79+
.get();
80+
assertEquals(200, response.getStatus());
81+
82+
InputStream entityStream = response.readEntity(InputStream.class);
83+
84+
// should consume the stream. must not throw here
85+
entityStream.close();
86+
// connection should be kept alive after consume
87+
assertEquals(0, getLeasedConnections());
88+
assertEquals(1, getAvailableConnections());
89+
90+
// must not throw here
91+
response.close();
92+
assertEquals(0, getLeasedConnections());
93+
assertEquals(1, getAvailableConnections());
94+
}
95+
96+
@Test
97+
public void testClosingConsumedStream() throws Exception {
98+
assertEquals(0, getOpenConnections());
99+
100+
Response response = target().path("productInfo")
101+
.request(MediaType.TEXT_PLAIN_TYPE)
102+
.get();
103+
assertEquals(200, response.getStatus());
104+
105+
InputStream entityStream = response.readEntity(InputStream.class);
106+
107+
consume(entityStream);
108+
109+
// connection should be kept alive after consume
110+
assertEquals(0, getLeasedConnections());
111+
assertEquals(1, getAvailableConnections());
112+
113+
entityStream.close();
114+
response.close();
115+
116+
assertEquals(0, getLeasedConnections());
117+
assertEquals(1, getAvailableConnections());
118+
}
119+
120+
@Test
121+
public void testClosingConsumedResponse() throws Exception {
122+
assertEquals(0, getOpenConnections());
123+
124+
Response response = target().path("productInfo")
125+
.request(MediaType.TEXT_PLAIN_TYPE)
126+
.get();
127+
assertEquals(200, response.getStatus());
128+
129+
InputStream entityStream = response.readEntity(InputStream.class);
130+
131+
consume(entityStream);
132+
133+
// connection should be kept alive after consume
134+
assertEquals(0, getLeasedConnections());
135+
assertEquals(1, getAvailableConnections());
136+
137+
response.close();
138+
entityStream.close();
139+
140+
assertEquals(0, getLeasedConnections());
141+
assertEquals(1, getAvailableConnections());
142+
}
143+
144+
@Test
145+
public void testBufferedMultipleReadEntity() throws Exception {
146+
assertEquals(0, getOpenConnections());
147+
148+
Response response = target().path("productInfo")
149+
.request(MediaType.TEXT_PLAIN_TYPE)
150+
.get();
151+
152+
response.bufferEntity();
153+
assertEquals(0, getLeasedConnections());
154+
assertEquals(1, getAvailableConnections());
155+
156+
assertEquals("foo\n", new String(response.readEntity(byte[].class), "us-ascii"));
157+
assertEquals("foo\n", response.readEntity(String.class));
158+
159+
response.close();
160+
161+
assertEquals(0, getLeasedConnections());
162+
assertEquals(1, getAvailableConnections());
163+
}
164+
165+
private static void consume(InputStream in) throws IOException {
166+
byte[] buffer = new byte[1024];
167+
for (int readden = in.read(buffer); readden >= 0; readden = in.read(buffer)) {
168+
}
169+
}
170+
171+
@Path("/")
172+
public static class TestResource {
173+
@GET
174+
@Path("/productInfo")
175+
@Produces(MediaType.TEXT_PLAIN)
176+
public String getProductInfo() {
177+
return "foo\n";
178+
}
179+
}
180+
}

0 commit comments

Comments
 (0)