diff --git a/client/src/main/java/org/asynchttpclient/netty/NettyResponse.java b/client/src/main/java/org/asynchttpclient/netty/NettyResponse.java index 31c7cb2f0..8d80bcbb1 100755 --- a/client/src/main/java/org/asynchttpclient/netty/NettyResponse.java +++ b/client/src/main/java/org/asynchttpclient/netty/NettyResponse.java @@ -224,9 +224,19 @@ public String getResponseBody() { return getResponseBody(withDefault(extractContentTypeCharsetAttribute(getContentType()), UTF_8)); } + /** + * 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. + */ + private byte[] sharedBodyBytes() { + return bodyParts.size() == 1 ? bodyParts.get(0).getBodyPartBytes() : getResponseBodyAsBytes(); + } + @Override public String getResponseBody(Charset charset) { - return new String(getResponseBodyAsBytes(), charset); + return new String(sharedBodyBytes(), charset); } @Override diff --git a/client/src/test/java/org/asynchttpclient/netty/NettyAsyncResponseTest.java b/client/src/test/java/org/asynchttpclient/netty/NettyAsyncResponseTest.java index 5172bae7a..5ce4982d5 100644 --- a/client/src/test/java/org/asynchttpclient/netty/NettyAsyncResponseTest.java +++ b/client/src/test/java/org/asynchttpclient/netty/NettyAsyncResponseTest.java @@ -19,7 +19,10 @@ import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.cookie.Cookie; import org.asynchttpclient.HttpResponseBodyPart; +import org.junit.jupiter.api.Test; +import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.Date; @@ -30,6 +33,7 @@ import static io.netty.handler.codec.http.HttpHeaderNames.SET_COOKIE; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; public class NettyAsyncResponseTest { @@ -90,4 +94,73 @@ public void testGetResponseBodyAsByteBuffer() { assertEquals("Hello World", body.toString(StandardCharsets.UTF_8)); body.release(); } + + @Test + public void testGetResponseBodyDecodesOnePartAndSplitPartsIdentically() { + byte[] utf8 = {'c', 'a', 'f', (byte) 0xC3, (byte) 0xA9, ' ', (byte) 0xC3, (byte) 0xBC, 'b', 'e', 'r'}; + String expected = new String(utf8, StandardCharsets.UTF_8); + // 0xC3 0xA9 encodes U+00E9; split between its two bytes so neither half decodes on its own + int split = 4; + + List onePart = new LinkedList<>(); + onePart.add(new EagerResponseBodyPart(Unpooled.wrappedBuffer(utf8), true)); + NettyResponse single = new NettyResponse(new NettyResponseStatus(null, null, null), null, onePart); + + List splitParts = new LinkedList<>(); + splitParts.add(new EagerResponseBodyPart(Unpooled.wrappedBuffer(utf8, 0, split), false)); + splitParts.add(new EagerResponseBodyPart(Unpooled.wrappedBuffer(utf8, split, utf8.length - split), true)); + NettyResponse multiple = new NettyResponse(new NettyResponseStatus(null, null, null), null, splitParts); + + assertEquals(expected, single.getResponseBody(StandardCharsets.UTF_8)); + assertEquals(expected, multiple.getResponseBody(StandardCharsets.UTF_8)); + } + + @Test + public void testGetResponseBodyReadsOnlyALazyPartsReadableRegion() throws IOException { + // A Lazy part's getBodyPartBytes returns just the readable region, not the whole backing array, so a + // single-part shortcut must go through it rather than reach for getBodyByteBuf().array(). + byte[] backing = "XXXHello WorldYYY".getBytes(StandardCharsets.UTF_8); + List bodyParts = new LinkedList<>(); + bodyParts.add(new LazyResponseBodyPart(Unpooled.wrappedBuffer(backing, 3, 11), true)); + NettyResponse response = new NettyResponse(new NettyResponseStatus(null, null, null), null, bodyParts); + + assertEquals("Hello World", response.getResponseBody(StandardCharsets.UTF_8)); + assertEquals("Hello World", + new String(response.getResponseBodyAsStream().readAllBytes(), StandardCharsets.UTF_8)); + } + + @Test + public void testGetResponseBodyAsBytesDoesNotShareTheBodyPartArray() { + List bodyParts = new LinkedList<>(); + bodyParts.add(new EagerResponseBodyPart(Unpooled.wrappedBuffer("Hello World".getBytes(StandardCharsets.UTF_8)), true)); + NettyResponse response = new NettyResponse(new NettyResponseStatus(null, null, null), null, bodyParts); + + // getResponseBody may decode a lone part in place, but getResponseBodyAsBytes hands the array to the + // caller, so it must keep copying rather than expose the part's own array. + assertNotSame(response.getResponseBodyAsBytes(), response.getResponseBodyAsBytes()); + assertNotSame(bodyParts.get(0).getBodyPartBytes(), response.getResponseBodyAsBytes()); + } + + @Test + public void testGetResponseBodyAsStreamDoesNotShareTheBodyPartArray() throws IOException { + List bodyParts = new LinkedList<>(); + bodyParts.add(new EagerResponseBodyPart(Unpooled.wrappedBuffer("Hello World".getBytes(StandardCharsets.UTF_8)), true)); + NettyResponse response = new NettyResponse(new NettyResponseStatus(null, null, null), null, bodyParts); + + // On JDK 11 ByteArrayInputStream.transferTo passes its own array to the OutputStream, so a stream over + // a part's array would put that array in the caller's hands. + byte[][] handedOut = new byte[1][]; + response.getResponseBodyAsStream().transferTo(new OutputStream() { + @Override + public void write(int b) { + } + + @Override + public void write(byte[] b, int off, int len) { + handedOut[0] = b; + } + }); + + assertNotSame(bodyParts.get(0).getBodyPartBytes(), handedOut[0]); + } }