Skip to content

Commit 3a24bee

Browse files
committed
Say what the shared body view aliases
1 parent 58cdc60 commit 3a24bee

3 files changed

Lines changed: 40 additions & 13 deletions

File tree

‎client/src/main/java/org/asynchttpclient/Response.java‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,19 @@ public interface Response {
6363
* body part's own array, it is the array reachable from the part handed to
6464
* {@link AsyncHandler#onBodyPartReceived}, the one each
6565
* {@link org.asynchttpclient.handler.TransferListener} is given by a
66-
* {@link org.asynchttpclient.handler.TransferCompletionHandler}, and the one
66+
* {@link org.asynchttpclient.handler.TransferCompletionHandler}, the one
67+
* {@link HttpResponseBodyPart#getBodyByteBuffer()} wraps for a
68+
* {@link org.asynchttpclient.handler.resumable.ResumableListener}, and the one
6769
* {@link #getResponseBodyAsByteBuf()} wraps. Writing to it changes what all of those see, and a write
6870
* through any of them changes what this returns.
6971
*
7072
* <p>Whether anything is shared at all is not something to rely on. It depends on how the body happened to
71-
* arrive - how the origin chunked it, whether a proxy re-chunked it, whether it was compressed - and on the
72-
* body parts the implementation was given, none of which is visible from here. The same body from the
73-
* same server may be shared on one response and copied on the next. No array identity is guaranteed between
74-
* calls either.
73+
* arrive - how the origin chunked it, whether a proxy re-chunked it, whether it was compressed - on how the
74+
* client happened to read it off the wire, which varies with connection age and client configuration, and on
75+
* the body parts the implementation was given. The same body from the same server may be shared on one
76+
* response and copied on the next. Which of the two a caller gets is the peer's choice rather than the
77+
* caller's, so code that writes to the array can behave one way against a friendly server and another way
78+
* against a hostile one. No array identity is guaranteed between calls either.
7579
*
7680
* <p>A caller that needs an array it may modify should copy what it receives. {@link
7781
* #getResponseBodyAsBytes()} is the accessor to reach for first, but it is implemented by whoever implements

‎client/src/main/java/org/asynchttpclient/netty/NettyResponse.java‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ public byte[] getResponseBodyAsBytes() {
196196
}
197197

198198
/**
199+
* {@inheritDoc}
200+
* <p>
199201
* Returns a lone body part's array; concatenates into one of its own when there are several, or an empty
200202
* array when there are none. Which of those a given response takes is not a property of the body: see
201203
* {@link Response#getResponseBodyAsBytesView()}, whose contract is deliberately weaker than this.
@@ -251,15 +253,15 @@ public String getResponseBody() {
251253
* documents it as read-only and names the other holders. {@link #getResponseBodyAsBytes()} stays the
252254
* copying accessor for callers who want an array of their own.
253255
* <p>
254-
* Private, and called directly by the accessors below rather than through
256+
* Private, and called directly by the string accessors rather than through
255257
* {@link #getResponseBodyAsBytesView()}, so that overriding the view does not silently change what this
256258
* response's text says as well.
257259
*/
258260
private byte[] sharedBodyBytes() {
259261
if (bodyParts.isEmpty()) {
260-
// A HEAD, a 204 or a 304 otherwise walks the aggregating path to allocate an empty array and a
261-
// buffer to wrap it, on every call. Nothing can be written through a zero-length array, so one
262-
// shared instance serves every empty body.
262+
// A response with no body - a HEAD, a 204, a 304, a discarded CONNECT failure, one aborted from a
263+
// handler - otherwise walks the aggregating path to allocate an empty array and a buffer to wrap it,
264+
// on every call. Nothing can be written through a zero-length array, so one instance serves them all.
263265
return EMPTY_BODY;
264266
}
265267
return bodyParts.size() == 1 ? bodyParts.get(0).getBodyPartBytes() : getResponseBodyAsBytes();

‎client/src/test/java/org/asynchttpclient/netty/NettyAsyncResponseTest.java‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,15 @@ public void testGetResponseBodyDecodesOnePartAndSplitPartsIdentically() {
109109
// 0xC3 0xA9 encodes U+00E9; split between its two bytes so neither half decodes on its own
110110
int split = 4;
111111

112+
// Clones onto the wire, so that utf8 stays an oracle rather than becoming a part's own storage.
112113
List<HttpResponseBodyPart> onePart = new LinkedList<>();
113-
onePart.add(new EagerResponseBodyPart(Unpooled.wrappedBuffer(utf8), true));
114+
onePart.add(new EagerResponseBodyPart(Unpooled.wrappedBuffer(utf8.clone()), true));
114115
NettyResponse single = new NettyResponse(new NettyResponseStatus(null, null, null), null, onePart);
115116

117+
byte[] wire = utf8.clone();
116118
List<HttpResponseBodyPart> splitParts = new LinkedList<>();
117-
splitParts.add(new EagerResponseBodyPart(Unpooled.wrappedBuffer(utf8, 0, split), false));
118-
splitParts.add(new EagerResponseBodyPart(Unpooled.wrappedBuffer(utf8, split, utf8.length - split), true));
119+
splitParts.add(new EagerResponseBodyPart(Unpooled.wrappedBuffer(wire, 0, split), false));
120+
splitParts.add(new EagerResponseBodyPart(Unpooled.wrappedBuffer(wire, split, wire.length - split), true));
119121
NettyResponse multiple = new NettyResponse(new NettyResponseStatus(null, null, null), null, splitParts);
120122

121123
assertEquals(expected, single.getResponseBody(StandardCharsets.UTF_8));
@@ -182,7 +184,10 @@ public void testGetResponseBodyAsBytesDoesNotShareTheBodyPartArray() {
182184
public void testGetResponseBodyAsBytesViewReturnsEmptyArray() {
183185
NettyResponse response = new NettyResponse(new NettyResponseStatus(null, null, null), null, new LinkedList<>());
184186

185-
assertArrayEquals(new byte[0], response.getResponseBodyAsBytesView());
187+
byte[] view = response.getResponseBodyAsBytesView();
188+
assertArrayEquals(new byte[0], view);
189+
// One shared instance rather than an allocation per call, which is the whole point of the branch.
190+
assertSame(view, response.getResponseBodyAsBytesView());
186191
}
187192

188193
@Test
@@ -198,6 +203,22 @@ public void testBodylessResponseHasAnEmptyByteBuf() {
198203
}
199204
}
200205

206+
@Test
207+
public void testOverridingTheViewLeavesTheResponseTextAlone() {
208+
// The string accessors go through a private helper rather than the overridable view, so a subclass that
209+
// hardens the view cannot silently change what this response's text says.
210+
List<HttpResponseBodyPart> bodyParts = new LinkedList<>();
211+
bodyParts.add(new EagerResponseBodyPart(Unpooled.wrappedBuffer("Hello World".getBytes(StandardCharsets.UTF_8)), true));
212+
NettyResponse response = new NettyResponse(new NettyResponseStatus(null, null, null), null, bodyParts) {
213+
@Override
214+
public byte[] getResponseBodyAsBytesView() {
215+
return "Goodbye".getBytes(StandardCharsets.UTF_8);
216+
}
217+
};
218+
219+
assertEquals("Hello World", response.getResponseBody(StandardCharsets.UTF_8));
220+
}
221+
201222
@Test
202223
public void testGetResponseBodyAsBytesViewDefaultImplementationDelegates() throws Throwable {
203224
byte[] expected = "Hello World".getBytes(StandardCharsets.UTF_8);

0 commit comments

Comments
 (0)