diff --git a/client/src/main/java/org/asynchttpclient/Response.java b/client/src/main/java/org/asynchttpclient/Response.java index 77512094d..3fc871b49 100644 --- a/client/src/main/java/org/asynchttpclient/Response.java +++ b/client/src/main/java/org/asynchttpclient/Response.java @@ -55,6 +55,39 @@ public interface Response { */ byte[] getResponseBodyAsBytes(); + /** + * Returns the entire response body as a byte array whose storage the implementation may share with whatever + * else holds the body. + * + *
The returned array must be treated as read-only, and the response is not its only holder. Where it is a + * body part's own array, it is the array reachable from the part handed to + * {@link AsyncHandler#onBodyPartReceived}, the one each + * {@link org.asynchttpclient.handler.TransferListener} is given by a + * {@link org.asynchttpclient.handler.TransferCompletionHandler}, and the one + * {@link #getResponseBodyAsByteBuf()} wraps. Writing to it changes what all of those see, and a write + * through any of them changes what this returns. + * + *
Whether anything is shared at all is not something to rely on. It depends on how the body happened to + * arrive - how the origin chunked it, whether a proxy re-chunked it, whether it was compressed - and on the + * body parts the implementation was given, none of which is visible from here. The same body from the + * same server may be shared on one response and copied on the next. No array identity is guaranteed between + * calls either. + * + *
A caller that needs an array it may modify should copy what it receives. {@link + * #getResponseBodyAsBytes()} is the accessor to reach for first, but it is implemented by whoever implements + * this interface, so read its contract rather than assuming it hands over an array of its own. + * + *
Implementation note: the default implementation of this method returns
+ * {@link #getResponseBodyAsBytes()}. An implementation that leaves that default in place must not implement
+ * {@code getResponseBodyAsBytes()} in terms of this method, or the two call each other. Overriding both is
+ * fine.
+ *
+ * @return the entire response body, possibly sharing storage with the response
+ */
+ default byte[] getResponseBodyAsBytesView() {
+ return getResponseBodyAsBytes();
+ }
+
/**
* Return the entire response body as a ByteBuffer.
*
diff --git a/client/src/main/java/org/asynchttpclient/netty/NettyResponse.java b/client/src/main/java/org/asynchttpclient/netty/NettyResponse.java
index 8d80bcbb1..0c080aee6 100755
--- a/client/src/main/java/org/asynchttpclient/netty/NettyResponse.java
+++ b/client/src/main/java/org/asynchttpclient/netty/NettyResponse.java
@@ -51,6 +51,8 @@
*/
public class NettyResponse implements Response {
+ private static final byte[] EMPTY_BODY = new byte[0];
+
private final List
+ * Whether a lone part hands over storage of its own is the part's business rather than this response's.
+ * {@link EagerResponseBodyPart} returns the array it holds; {@link LazyResponseBodyPart} copies out of its
+ * buffer on every call, so a response made of lazy parts never shares whatever this says.
+ */
+ @Override
+ public byte[] getResponseBodyAsBytesView() {
+ return sharedBodyBytes();
+ }
+
@Override
public ByteBuffer getResponseBodyAsByteBuffer() {
@@ -225,12 +241,25 @@ public String getResponseBody() {
}
/**
- * The body as bytes, for callers that keep the array to themselves. A lone part's own array is returned
- * rather than a copy of it, so a caller that let it out would let the part's buffer be mutated through it;
- * {@link #getResponseBodyAsBytes()} is the copying variant for those. Several parts are concatenated
- * because a multi-byte character can straddle a part boundary.
+ * The body as bytes, without a copy where there is one part to take it from. Several parts are concatenated
+ * because a multi-byte character can straddle a part boundary, which is why the string accessors cannot
+ * simply decode the first part.
+ *
+ * The array does leave the client, through {@link #getResponseBodyAsBytesView()}, which is why that method
+ * documents it as read-only and names the other holders. {@link #getResponseBodyAsBytes()} stays the
+ * copying accessor for callers who want an array of their own.
+ *
+ * Private, and called directly by the accessors below rather than through
+ * {@link #getResponseBodyAsBytesView()}, so that overriding the view does not silently change what this
+ * response's text says as well.
*/
private byte[] sharedBodyBytes() {
+ if (bodyParts.isEmpty()) {
+ // A HEAD, a 204 or a 304 otherwise walks the aggregating path to allocate an empty array and a
+ // buffer to wrap it, on every call. Nothing can be written through a zero-length array, so one
+ // shared instance serves every empty body.
+ return EMPTY_BODY;
+ }
return bodyParts.size() == 1 ? bodyParts.get(0).getBodyPartBytes() : getResponseBodyAsBytes();
}
diff --git a/client/src/test/java/org/asynchttpclient/netty/NettyAsyncResponseTest.java b/client/src/test/java/org/asynchttpclient/netty/NettyAsyncResponseTest.java
index 5ce4982d5..0b6320d4f 100644
--- a/client/src/test/java/org/asynchttpclient/netty/NettyAsyncResponseTest.java
+++ b/client/src/test/java/org/asynchttpclient/netty/NettyAsyncResponseTest.java
@@ -19,10 +19,13 @@
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.cookie.Cookie;
import org.asynchttpclient.HttpResponseBodyPart;
+import org.asynchttpclient.Response;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.OutputStream;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
@@ -32,9 +35,13 @@
import java.util.TimeZone;
import static io.netty.handler.codec.http.HttpHeaderNames.SET_COOKIE;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
public class NettyAsyncResponseTest {
@@ -113,6 +120,8 @@ public void testGetResponseBodyDecodesOnePartAndSplitPartsIdentically() {
assertEquals(expected, single.getResponseBody(StandardCharsets.UTF_8));
assertEquals(expected, multiple.getResponseBody(StandardCharsets.UTF_8));
+ assertArrayEquals(utf8, single.getResponseBodyAsBytesView());
+ assertArrayEquals(utf8, multiple.getResponseBodyAsBytesView());
}
@Test
@@ -130,15 +139,64 @@ public void testGetResponseBodyReadsOnlyALazyPartsReadableRegion() throws IOExce
}
@Test
- public void testGetResponseBodyAsBytesDoesNotShareTheBodyPartArray() {
+ public void testGetResponseBodyAsBytesViewSharesOneEagerPart() {
+ // NettyResponse's own behaviour, not the interface contract: Response#getResponseBodyAsBytesView
+ // guarantees no identity, deliberately, because whether a body arrives as one part is not up to it.
+ // What is worth pinning here is that when this response can share, it does, and does not copy instead.
List