From 2267ef26b93464549271a2ad33e53bf8efd8b55d Mon Sep 17 00:00:00 2001 From: Serkan Turgut Date: Mon, 10 Jun 2019 18:13:41 -0700 Subject: [PATCH] HTTPCLIENT-1992: Impossible to access trailer-headers available in chunked transfer-encoding 1. Implementing getTrailers() in ResponseEntityProxy which will return a Supplier which propagates the output of ChunkedInputStream.getFooters(), otherwise it returns a empty list. 2. Fixing a typo in ResponseEntityProxy.enhance() method name. --- .../http/impl/classic/MainClientExec.java | 2 +- .../http/impl/classic/MinimalHttpClient.java | 2 +- .../impl/classic/ResponseEntityProxy.java | 29 ++++- .../impl/classic/TestResponseEntityProxy.java | 108 ++++++++++++++++++ 4 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestResponseEntityProxy.java diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MainClientExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MainClientExec.java index 93d3b340e..aaef3465d 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MainClientExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MainClientExec.java @@ -135,7 +135,7 @@ public final class MainClientExec implements ExecChainHandler { execRuntime.releaseEndpoint(); return new CloseableHttpResponse(response, null); } - ResponseEntityProxy.enchance(response, execRuntime); + ResponseEntityProxy.enhance(response, execRuntime); return new CloseableHttpResponse(response, execRuntime); } catch (final ConnectionShutdownException ex) { final InterruptedIOException ioex = new InterruptedIOException( diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MinimalHttpClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MinimalHttpClient.java index 2c76aa90e..4164f98c6 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MinimalHttpClient.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MinimalHttpClient.java @@ -160,7 +160,7 @@ public class MinimalHttpClient extends CloseableHttpClient { execRuntime.releaseEndpoint(); return new CloseableHttpResponse(response, null); } - ResponseEntityProxy.enchance(response, execRuntime); + ResponseEntityProxy.enhance(response, execRuntime); return new CloseableHttpResponse(response, execRuntime); } catch (final ConnectionShutdownException ex) { final InterruptedIOException ioex = new InterruptedIOException("Connection has been shut down"); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ResponseEntityProxy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ResponseEntityProxy.java index fb6182b59..b70211ea7 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ResponseEntityProxy.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ResponseEntityProxy.java @@ -31,10 +31,15 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.SocketException; +import java.util.Arrays; +import java.util.List; import org.apache.hc.client5.http.classic.ExecRuntime; +import org.apache.hc.core5.function.Supplier; import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.impl.io.ChunkedInputStream; import org.apache.hc.core5.http.io.EofSensorInputStream; import org.apache.hc.core5.http.io.EofSensorWatcher; import org.apache.hc.core5.http.io.entity.HttpEntityWrapper; @@ -43,7 +48,7 @@ class ResponseEntityProxy extends HttpEntityWrapper implements EofSensorWatcher private final ExecRuntime execRuntime; - public static void enchance(final ClassicHttpResponse response, final ExecRuntime execRuntime) { + public static void enhance(final ClassicHttpResponse response, final ExecRuntime execRuntime) { final HttpEntity entity = response.getEntity(); if (entity != null && entity.isStreaming() && execRuntime != null) { response.setEntity(new ResponseEntityProxy(entity, execRuntime)); @@ -150,4 +155,26 @@ class ResponseEntityProxy extends HttpEntityWrapper implements EofSensorWatcher return false; } + @Override + public Supplier> getTrailers() { + try { + final InputStream underlyingStream = super.getContent(); + return new Supplier>() { + @Override + public List get() { + final Header[] footers; + if (underlyingStream instanceof ChunkedInputStream) { + final ChunkedInputStream chunkedInputStream = (ChunkedInputStream) underlyingStream; + footers = chunkedInputStream.getFooters(); + } else { + footers = new Header[0]; + } + return Arrays.asList(footers); + } + }; + } catch (final IOException e) { + throw new IllegalStateException("Unable to retrieve input stream", e); + } + } + } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestResponseEntityProxy.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestResponseEntityProxy.java new file mode 100644 index 000000000..081988735 --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestResponseEntityProxy.java @@ -0,0 +1,108 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.impl.classic; + +import org.apache.hc.client5.http.classic.ExecRuntime; +import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.Header; +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.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.List; + +public class TestResponseEntityProxy { + + @Mock + private ClassicHttpResponse response; + @Mock + private ExecRuntime execRuntime; + @Mock + private HttpEntity entity; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + Mockito.when(entity.isStreaming()).thenReturn(Boolean.TRUE); + Mockito.when(response.getEntity()).thenReturn(entity); + } + + @Test + public void testGetTrailersWithNoChunkedInputStream() throws Exception { + final ByteArrayInputStream inputStream = new ByteArrayInputStream("Test payload".getBytes()); + Mockito.when(entity.getContent()).thenReturn(inputStream); + final ArgumentCaptor httpEntityArgumentCaptor = ArgumentCaptor.forClass(HttpEntity.class); + + ResponseEntityProxy.enhance(response, execRuntime); + + Mockito.verify(response).setEntity(httpEntityArgumentCaptor.capture()); + final HttpEntity wrappedEntity = httpEntityArgumentCaptor.getValue(); + + final InputStream is = wrappedEntity.getContent(); + while (is.read() != -1) {} // read until the end + final Supplier> trailers = wrappedEntity.getTrailers(); + + Assert.assertTrue(trailers.get().isEmpty()); + } + + @Test + public void testGetTrailersWithChunkedInputStream() 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); + + Mockito.when(entity.getContent()).thenReturn(chunkedInputStream); + final ArgumentCaptor httpEntityArgumentCaptor = ArgumentCaptor.forClass(HttpEntity.class); + + ResponseEntityProxy.enhance(response, execRuntime); + + Mockito.verify(response).setEntity(httpEntityArgumentCaptor.capture()); + final HttpEntity wrappedEntity = httpEntityArgumentCaptor.getValue(); + + final InputStream is = wrappedEntity.getContent(); + while (is.read() != -1) {} // consume the stream so it can reach to trailers and parse + final Supplier> trailers = wrappedEntity.getTrailers(); + final List headers = trailers.get(); + + Assert.assertEquals(1, headers.size()); + final Header header = headers.get(0); + Assert.assertEquals("X-Test-Trailer-Header", header.getName()); + Assert.assertEquals("test", header.getValue()); + } +} \ No newline at end of file