ResponseEntityProxy.writeTo(null) leaves connections in the correct state

Previously writeTo would conditionally delegate to the wrapped
entity if the provided outputstream was non-null, however in the
null case the entity would not be drained and the connection would
be released potentially with bytes remaining. If this occurs in
practice, it may result in timeouts as the server expects to write
data to the response while the client is attempting to send a
request.
This commit is contained in:
Carter Kozak 2022-06-13 12:34:37 -04:00 committed by Oleg Kalnichevski
parent 3bd017cb0a
commit 944e308a52
2 changed files with 70 additions and 3 deletions

View File

@ -94,9 +94,7 @@ class ResponseEntityProxy extends HttpEntityWrapper implements EofSensorWatcher
@Override
public void writeTo(final OutputStream outStream) throws IOException {
try {
if (outStream != null) {
super.writeTo(outStream);
}
super.writeTo(outStream != null ? outStream : NullOutputStream.INSTANCE);
releaseConnection();
} catch (final IOException | RuntimeException ex) {
discardConnection();
@ -188,4 +186,43 @@ class ResponseEntityProxy extends HttpEntityWrapper implements EofSensorWatcher
cleanup();
}
}
private static final class NullOutputStream extends OutputStream {
private static final NullOutputStream INSTANCE = new NullOutputStream();
private NullOutputStream() {}
@Override
public void write(@SuppressWarnings("unused") final int byteValue) {
// no-op
}
@Override
public void write(@SuppressWarnings("unused") final byte[] buffer) {
// no-op
}
@Override
public void write(
@SuppressWarnings("unused") final byte[] buffer,
@SuppressWarnings("unused") final int off,
@SuppressWarnings("unused") final int len) {
// no-op
}
@Override
public void flush() {
// no-op
}
@Override
public void close() {
// no-op
}
@Override
public String toString() {
return "NullOutputStream{}";
}
}
}

View File

@ -39,6 +39,8 @@ import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.impl.io.ChunkedInputStream;
import org.apache.hc.core5.http.impl.io.SessionInputBufferImpl;
import org.apache.hc.core5.http.io.SessionInputBuffer;
import org.apache.hc.core5.http.io.entity.BasicHttpEntity;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -105,4 +107,32 @@ public class TestResponseEntityProxy {
Assertions.assertEquals("X-Test-Trailer-Header", header.getName());
Assertions.assertEquals("test", header.getValue());
}
@Test
public void testWriteToNullDrainsAndReleasesStream() throws Exception {
final SessionInputBuffer sessionInputBuffer = new SessionInputBufferImpl(100);
final ByteArrayInputStream inputStream = new ByteArrayInputStream("0\r\nX-Test-Trailer-Header: test\r\n".getBytes());
final ChunkedInputStream chunkedInputStream = new ChunkedInputStream(sessionInputBuffer, inputStream);
final CloseableHttpResponse resp = new CloseableHttpResponse(new BasicClassicHttpResponse(200), execRuntime);
final HttpEntity entity = new BasicHttpEntity(chunkedInputStream, null, true);
Assertions.assertTrue(entity.isStreaming());
resp.setEntity(entity);
ResponseEntityProxy.enhance(resp, execRuntime);
final HttpEntity wrappedEntity = resp.getEntity();
wrappedEntity.writeTo(null);
Mockito.verify(execRuntime).releaseEndpoint();
final Supplier<List<? extends Header>> trailers = wrappedEntity.getTrailers();
final List<? extends Header> headers = trailers.get();
Assertions.assertEquals(1, headers.size());
final Header header = headers.get(0);
Assertions.assertEquals("X-Test-Trailer-Header", header.getName());
Assertions.assertEquals("test", header.getValue());
}
}