You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
copies the response body twice before returning it as a byte array:
EagerResponseBodyPart copies every incoming Netty ByteBuf into an
AHC-owned byte[].
NettyResponse.getResponseBodyAsBytes() calls getResponseBodyAsByteBuffer(), which allocates another array for the
complete body and copies all body parts into it.
The second copy is unnecessary when a response arrived in one chunk because
the EagerResponseBodyPart already owns one complete AHC-managed array.
#2303 removed this extra copy from getResponseBody(Charset), but deliberately
kept it in getResponseBodyAsBytes() because that method hands a mutable array
to application code. Byte-array consumers therefore cannot opt into the same
optimization even when they can guarantee that they will not mutate the body.
Goal
Add an explicit opt-in API that may return shared response-body storage. Keep
all existing behavior of getResponseBodyAsBytes() unchanged.
Proposed API
Add this default method to org.asynchttpclient.Response:
/** * 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 */defaultbyte[] getResponseBodyAsBytesView() {
returngetResponseBodyAsBytes();
}
A default method keeps third-party Response implementations binary- and
source-compatible. Implementations that cannot expose shared storage preserve
the existing safe behavior. Aliasing is enabled explicitly by the caller.
The name and Javadoc must make it clear that the returned array can be shared
and must be treated as read-only.
Use getBodyPartBytes(); perform the one necessary copy from the ByteBuf, but do not allocate a second aggregate array
Multiple parts
Use the existing concatenation through getResponseBodyAsBytes()
The implementation must call getBodyPartBytes() and must not access ByteBuf.array() directly. A ByteBuf can be direct, can be a slice with a
non-zero readerIndex or arrayOffset, and its backing array can contain bytes
outside the readable region.
This preserves the string-decoding optimization from #2303 while exposing it to
byte-array consumers only through the new opt-in method.
Required constraints
The change must not:
change the behavior of getResponseBodyAsBytes();
change the behavior of getResponseBodyAsByteBuffer();
make any existing byte[] API implicitly shared;
retain the original network ByteBuf or change its reference count;
access ByteBuf.array() directly;
change ResponseBodyPartFactory.EAGER or ResponseBodyPartFactory.LAZY;
add synchronization or atomic operations to the response hot path;
cache multi-part concatenation as part of this change.
getResponseBodyAsBytes() must continue to return an independently owned
array. Mutating an array returned by the existing method must not affect the
response.
Tests
One EAGER part
Assert that the view is the exact array owned by the part.
Assert that repeated view calls return the same array for this NettyResponse shape. This is an implementation performance property, not a
general Response API identity guarantee.
Prove that two calls to getResponseBodyAsBytes() return distinct arrays.
Prove that mutating the old method's result does not affect the response.
Lazy part over a slice/direct buffer
Create a LazyResponseBodyPart over the readable Hello World region in XXXHello WorldYYY and assert that the view contains no surrounding bytes.
Also verify that the call does not change readerIndex, writerIndex, or refCnt. Cover a direct buffer as well.
Multiple parts
Verify correct concatenation order with no missing or duplicate bytes.
Split a multi-byte UTF-8 character across two parts.
Verify that getResponseBody(Charset) produces the same result for one and
multiple parts.
Empty body
Verify that the view returns an empty array.
Default implementation
Add a test for a Response implementation that does not override the new
method. It must delegate to getResponseBodyAsBytes() without requiring a
third-party implementation change.
Netty buffer lifecycle and reference counts are unchanged.
mvnw.cmd clean verify passes on JDK 11, including Error Prone, NullAway,
and Revapi.
The pull request includes allocation benchmark results.
Out of scope
Removing the initial ByteBuf -> EagerResponseBodyPart copy.
Preallocating from Content-Length.
A new multi-chunk accumulator.
Caching the aggregated multi-part body.
Changing the LAZY response lifecycle.
A zero-copy InputStream.
Changing the semantics of getResponseBodyAsBytes() or getResponseBodyAsByteBuffer().
After release, downstream byte-array consumers can opt in by replacing a method
reference with response::getResponseBodyAsBytesView. For single-part responses,
JFR should then show the aggregate allocation in NettyResponse.getResponseBodyAsByteBuffer() disappearing; the EagerResponseBodyPart allocation remains.
Problem
The standard response path
copies the response body twice before returning it as a byte array:
EagerResponseBodyPartcopies every incoming NettyByteBufinto anAHC-owned
byte[].NettyResponse.getResponseBodyAsBytes()callsgetResponseBodyAsByteBuffer(), which allocates another array for thecomplete body and copies all body parts into it.
The second copy is unnecessary when a response arrived in one chunk because
the
EagerResponseBodyPartalready owns one complete AHC-managed array.#2303 removed this extra copy from
getResponseBody(Charset), but deliberatelykept it in
getResponseBodyAsBytes()because that method hands a mutable arrayto application code. Byte-array consumers therefore cannot opt into the same
optimization even when they can guarantee that they will not mutate the body.
Goal
Add an explicit opt-in API that may return shared response-body storage. Keep
all existing behavior of
getResponseBodyAsBytes()unchanged.Proposed API
Add this default method to
org.asynchttpclient.Response:A default method keeps third-party
Responseimplementations binary- andsource-compatible. Implementations that cannot expose shared storage preserve
the existing safe behavior. Aliasing is enabled explicitly by the caller.
The name and Javadoc must make it clear that the returned array can be shared
and must be treated as read-only.
NettyResponse implementation
NettyResponseshould override the method:Expected behavior:
EagerResponseBodyPartbyte[]without copyingLazyResponseBodyPartgetBodyPartBytes(); perform the one necessary copy from theByteBuf, but do not allocate a second aggregate arraygetResponseBodyAsBytes()The implementation must call
getBodyPartBytes()and must not accessByteBuf.array()directly. AByteBufcan be direct, can be a slice with anon-zero
readerIndexorarrayOffset, and its backing array can contain bytesoutside the readable region.
Integration with #2303
Remove or replace the existing private
sharedBodyBytes()method so the fastpath has a single implementation:
This preserves the string-decoding optimization from #2303 while exposing it to
byte-array consumers only through the new opt-in method.
Required constraints
The change must not:
getResponseBodyAsBytes();getResponseBodyAsByteBuffer();byte[]API implicitly shared;ByteBufor change its reference count;ByteBuf.array()directly;ResponseBodyPartFactory.EAGERorResponseBodyPartFactory.LAZY;getResponseBodyAsBytes()must continue to return an independently ownedarray. Mutating an array returned by the existing method must not affect the
response.
Tests
One EAGER part
NettyResponseshape. This is an implementation performance property, not ageneral
ResponseAPI identity guarantee.Defensive-copy preservation
part's array.
getResponseBodyAsBytes()return distinct arrays.Lazy part over a slice/direct buffer
Create a
LazyResponseBodyPartover the readableHello Worldregion inXXXHello WorldYYYand assert that the view contains no surrounding bytes.Also verify that the call does not change
readerIndex,writerIndex, orrefCnt. Cover a direct buffer as well.Multiple parts
getResponseBody(Charset)produces the same result for one andmultiple parts.
Empty body
Verify that the view returns an empty array.
Default implementation
Add a test for a
Responseimplementation that does not override the newmethod. It must delegate to
getResponseBodyAsBytes()without requiring athird-party implementation change.
Benchmark
Compare:
Dataset matrix:
Expected result:
getResponseBodyAsBytes()behavior and performance remain unchanged.No hard numerical CI threshold is required. Include allocation benchmark
results in the pull request.
Acceptance criteria
Responseimplementations do not need to change.EagerResponseBodyPartis returned without another copy.getResponseBodyAsBytes()still returns an independent array.getResponseBody(Charset)optimization from Decode a lone response body part in place #2303 is preserved.mvnw.cmd clean verifypasses on JDK 11, including Error Prone, NullAway,and Revapi.
Out of scope
ByteBuf->EagerResponseBodyPartcopy.Content-Length.InputStream.getResponseBodyAsBytes()orgetResponseBodyAsByteBuffer().After release, downstream byte-array consumers can opt in by replacing a method
reference with
response::getResponseBodyAsBytesView. For single-part responses,JFR should then show the aggregate allocation in
NettyResponse.getResponseBodyAsByteBuffer()disappearing; theEagerResponseBodyPartallocation remains.