Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions client/src/main/java/org/asynchttpclient/Response.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@
*/
byte[] getResponseBodyAsBytes();

/**
* Returns the entire response body as a byte array that may share its storage with this response.
*
* <p>The returned array must be treated as read-only. Modifying it may change the content subsequently returned
* by this response's other body accessors.
*
* <p>The implementation is not required to return shared storage. Depending on the response representation, this
* method may still return a copy. No array identity is guaranteed between calls.
*
* <p>Use {@link #getResponseBodyAsBytes()} when an independently owned, mutable array is required.
*
* @return the entire response body as a possibly shared byte array
*/
default byte[] getResponseBodyAsBytesView() {
return getResponseBodyAsBytes();
}

/**
* Return the entire response body as a ByteBuffer.
*
Expand Down Expand Up @@ -106,7 +123,7 @@
String getContentType();

/**
* @param name the header name

Check warning on line 126 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / compile-and-check

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 126 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 25)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 126 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 21)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 126 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 17)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 126 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 11)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 126 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 21)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 126 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 17)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 126 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 11)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 126 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 25)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.
* @return the first response header value
*/
String getHeader(CharSequence name);
Expand Down Expand Up @@ -137,7 +154,7 @@
String toString();

/**
* @return the list of {@link Cookie}.

Check warning on line 157 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / compile-and-check

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 157 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 25)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 157 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 21)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 157 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 17)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 157 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 11)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 157 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 21)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 157 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 17)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 157 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 11)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 157 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 25)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.
*/
List<Cookie> getCookies();

Expand Down Expand Up @@ -206,7 +223,7 @@
}

/**
* @param bodyPart a body part (possibly empty, but will be filtered out)

Check warning on line 226 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / compile-and-check

[MissingSummary] A summary line is required on public/protected Javadocs.

Check warning on line 226 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 25)

[MissingSummary] A summary line is required on public/protected Javadocs.

Check warning on line 226 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 21)

[MissingSummary] A summary line is required on public/protected Javadocs.

Check warning on line 226 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 17)

[MissingSummary] A summary line is required on public/protected Javadocs.

Check warning on line 226 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 11)

[MissingSummary] A summary line is required on public/protected Javadocs.

Check warning on line 226 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 21)

[MissingSummary] A summary line is required on public/protected Javadocs.

Check warning on line 226 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 17)

[MissingSummary] A summary line is required on public/protected Javadocs.

Check warning on line 226 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 11)

[MissingSummary] A summary line is required on public/protected Javadocs.

Check warning on line 226 in client/src/main/java/org/asynchttpclient/Response.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 25)

[MissingSummary] A summary line is required on public/protected Javadocs.
*/
public void accumulate(HttpResponseBodyPart bodyPart) {
if (bodyPart.length() > 0) {
Expand Down
17 changes: 6 additions & 11 deletions client/src/main/java/org/asynchttpclient/netty/NettyResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ public byte[] getResponseBodyAsBytes() {
return getResponseBodyAsByteBuffer().array();
}

@Override
public byte[] getResponseBodyAsBytesView() {
return bodyParts.size() == 1 ? bodyParts.get(0).getBodyPartBytes() : getResponseBodyAsBytes();
}

@Override
public ByteBuffer getResponseBodyAsByteBuffer() {

Expand Down Expand Up @@ -224,19 +229,9 @@ 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(sharedBodyBytes(), charset);
return new String(getResponseBodyAsBytesView(), charset);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -113,32 +120,108 @@ 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
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);
ByteBuf slice = Unpooled.wrappedBuffer(backing).slice(3, 11);
int readerIndex = slice.readerIndex();
int writerIndex = slice.writerIndex();
int refCnt = slice.refCnt();
try {
List<HttpResponseBodyPart> bodyParts = new LinkedList<>();
bodyParts.add(new LazyResponseBodyPart(slice, true));
NettyResponse response = new NettyResponse(new NettyResponseStatus(null, null, null), null, bodyParts);

assertArrayEquals("Hello World".getBytes(StandardCharsets.UTF_8), response.getResponseBodyAsBytesView());
assertEquals("Hello World", response.getResponseBody(StandardCharsets.UTF_8));
assertEquals("Hello World",
new String(response.getResponseBodyAsStream().readAllBytes(), StandardCharsets.UTF_8));
assertEquals(readerIndex, slice.readerIndex());
assertEquals(writerIndex, slice.writerIndex());
assertEquals(refCnt, slice.refCnt());
} finally {
slice.release();
}
}

@Test
public void testGetResponseBodyAsBytesViewReadsDirectLazyPart() {
byte[] backing = "XXXHello WorldYYY".getBytes(StandardCharsets.UTF_8);
ByteBuf direct = Unpooled.directBuffer(backing.length);
direct.writeBytes(backing);
ByteBuf slice = direct.slice(3, 11);
int readerIndex = slice.readerIndex();
int writerIndex = slice.writerIndex();
int refCnt = slice.refCnt();
try {
List<HttpResponseBodyPart> bodyParts = new LinkedList<>();
bodyParts.add(new LazyResponseBodyPart(slice, true));
NettyResponse response = new NettyResponse(new NettyResponseStatus(null, null, null), null, bodyParts);

assertArrayEquals("Hello World".getBytes(StandardCharsets.UTF_8), response.getResponseBodyAsBytesView());
assertEquals(readerIndex, slice.readerIndex());
assertEquals(writerIndex, slice.writerIndex());
assertEquals(refCnt, slice.refCnt());
} finally {
direct.release();
}
}

@Test
public void testGetResponseBodyAsBytesViewSharesOneEagerPart() {
List<HttpResponseBodyPart> bodyParts = new LinkedList<>();
bodyParts.add(new LazyResponseBodyPart(Unpooled.wrappedBuffer(backing, 3, 11), true));
bodyParts.add(new EagerResponseBodyPart(Unpooled.wrappedBuffer("Hello World".getBytes(StandardCharsets.UTF_8)), 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));
byte[] view = response.getResponseBodyAsBytesView();
assertSame(bodyParts.get(0).getBodyPartBytes(), view);
assertSame(view, response.getResponseBodyAsBytesView());
}

@Test
public void testGetResponseBodyAsBytesDoesNotShareTheBodyPartArray() {
byte[] expected = "Hello World".getBytes(StandardCharsets.UTF_8);
List<HttpResponseBodyPart> bodyParts = new LinkedList<>();
bodyParts.add(new EagerResponseBodyPart(Unpooled.wrappedBuffer("Hello World".getBytes(StandardCharsets.UTF_8)), true));
bodyParts.add(new EagerResponseBodyPart(Unpooled.wrappedBuffer(expected), 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());
byte[] firstCopy = response.getResponseBodyAsBytes();
byte[] secondCopy = response.getResponseBodyAsBytes();
assertNotSame(firstCopy, secondCopy);
assertNotSame(bodyParts.get(0).getBodyPartBytes(), firstCopy);

firstCopy[0] = 'X';
assertArrayEquals(expected, response.getResponseBodyAsBytes());
assertArrayEquals(expected, response.getResponseBodyAsBytesView());
}

@Test
public void testGetResponseBodyAsBytesViewReturnsEmptyArray() {
NettyResponse response = new NettyResponse(new NettyResponseStatus(null, null, null), null, new LinkedList<>());

assertArrayEquals(new byte[0], response.getResponseBodyAsBytesView());
}

@Test
public void testGetResponseBodyAsBytesViewDefaultImplementationDelegates() throws Throwable {
byte[] expected = "Hello World".getBytes(StandardCharsets.UTF_8);
Response response = mock(Response.class);
when(response.getResponseBodyAsBytes()).thenReturn(expected);

byte[] actual = (byte[]) MethodHandles.privateLookupIn(Response.class, MethodHandles.lookup())
.findSpecial(Response.class, "getResponseBodyAsBytesView", MethodType.methodType(byte[].class), Response.class)
.bindTo(response)
.invokeExact();

assertSame(expected, actual);
}

@Test
Expand Down
Loading