Skip to content

Commit 02a6a46

Browse files
committed
Workaround JNH InputStream.available() == 1 for no entity
Signed-off-by: jansupol <jan.supol@oracle.com>
1 parent 665655f commit 02a6a46

File tree

3 files changed

+186
-1
lines changed

3 files changed

+186
-1
lines changed

connectors/jnh-connector/src/main/java/org/glassfish/jersey/jnh/connector/JavaNetHttpConnector.java

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
import java.util.Optional;
5656
import java.util.concurrent.CompletableFuture;
5757
import java.util.concurrent.Future;
58+
import java.util.concurrent.locks.Lock;
59+
import java.util.concurrent.locks.ReentrantLock;
5860
import java.util.logging.Logger;
5961

6062
/**
@@ -253,7 +255,12 @@ private ClientResponse buildClientResponse(ClientRequest request, HttpResponse<I
253255
headers.put(headerName, entry.getValue());
254256
}
255257
}
256-
clientResponse.setEntityStream(response.body());
258+
final InputStream body = response.body();
259+
try {
260+
clientResponse.setEntityStream(body.available() != 1 ? body : new FirstByteCachingStream(body));
261+
} catch (IOException ioe) {
262+
throw new ProcessingException(ioe);
263+
}
257264
return clientResponse;
258265
}
259266

@@ -329,4 +336,67 @@ private SSLParameters getSslParameters(Client client) {
329336
}
330337
}
331338
}
339+
340+
/*
341+
* The JDK stream returns available() == 1 even when read() == -1
342+
* This class is to prevent it.
343+
* Otherwise, the MBR is not found for 204
344+
* See https://github.com/eclipse-ee4j/jersey/issues/5307
345+
*/
346+
private static class FirstByteCachingStream extends InputStream {
347+
private final InputStream inner;
348+
private volatile int zero = -1; // int on zero index
349+
private final Lock lock = new ReentrantLock();
350+
351+
private FirstByteCachingStream(InputStream inner) {
352+
this.inner = inner;
353+
}
354+
355+
@Override
356+
public int read() throws IOException {
357+
lock.lock();
358+
final int r = zero != -1 ? zero : inner.read();
359+
zero = -1;
360+
lock.unlock();
361+
return r;
362+
}
363+
364+
@Override
365+
public int read(byte[] b, int off, int len) throws IOException {
366+
lock.lock();
367+
int r;
368+
if (zero != -1) {
369+
b[off] = (byte) (zero & 0xFF);
370+
r = inner.read(b, off + 1, len - 1);
371+
} else {
372+
r = inner.read(b, off, len);
373+
}
374+
zero = -1;
375+
lock.unlock();
376+
return r;
377+
378+
}
379+
380+
@Override
381+
public int available() throws IOException {
382+
lock.lock();
383+
if (zero != -1) {
384+
lock.unlock();
385+
return 1;
386+
}
387+
388+
int available = inner.available();
389+
if (available != 1) {
390+
return available;
391+
}
392+
393+
zero = inner.read();
394+
if (zero == -1) {
395+
available = 0;
396+
}
397+
lock.unlock();
398+
return available;
399+
}
400+
}
401+
332402
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0, which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the
10+
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11+
* version 2 with the GNU Classpath Exception, which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
*/
16+
17+
package org.glassfish.jersey.jnh.connector;
18+
19+
import org.junit.jupiter.api.Assertions;
20+
import org.junit.jupiter.api.Test;
21+
22+
import java.io.ByteArrayInputStream;
23+
import java.io.InputStream;
24+
import java.lang.reflect.Constructor;
25+
26+
class FirstByteCachingStreamTest {
27+
private static InputStream createFirstByteCachingStream(InputStream inner) throws Exception {
28+
Class[] classes = JavaNetHttpConnector.class.getDeclaredClasses();
29+
for (Class<?> clazz : classes) {
30+
if (clazz.getName().contains("FirstByteCachingStream")) {
31+
Constructor constructor = clazz.getDeclaredConstructor(InputStream.class);
32+
constructor.setAccessible(true);
33+
return (InputStream) constructor.newInstance(inner);
34+
}
35+
}
36+
throw new IllegalArgumentException("JavaNetHttpConnector inner class FirstByteCachingStream not found");
37+
}
38+
39+
@Test
40+
void testNoByte() throws Exception {
41+
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(new byte[0]);
42+
InputStream testIs = createFirstByteCachingStream(byteArrayInputStream);
43+
Assertions.assertEquals(0, testIs.available());
44+
}
45+
46+
@Test
47+
void testOneByte() throws Exception {
48+
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(new byte[]{'A'});
49+
InputStream testIs = createFirstByteCachingStream(byteArrayInputStream);
50+
Assertions.assertEquals(1, testIs.available());
51+
Assertions.assertEquals(1, testIs.available()); // idempotency
52+
Assertions.assertEquals('A', testIs.read());
53+
Assertions.assertEquals(0, testIs.available());
54+
}
55+
56+
@Test
57+
void testTwoBytes() throws Exception {
58+
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(new byte[]{'A', 'B'});
59+
InputStream testIs = createFirstByteCachingStream(byteArrayInputStream);
60+
Assertions.assertEquals(2, testIs.available());
61+
Assertions.assertEquals(2, testIs.available()); // idempotency
62+
Assertions.assertEquals('A', testIs.read());
63+
Assertions.assertEquals(1, testIs.available());
64+
Assertions.assertEquals(1, testIs.available()); // idempotency
65+
Assertions.assertEquals('B', testIs.read());
66+
Assertions.assertEquals(0, testIs.available());
67+
}
68+
69+
@Test
70+
void testTwoBytesReadAtOnce() throws Exception {
71+
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(new byte[]{'A', 'B'});
72+
InputStream testIs = createFirstByteCachingStream(byteArrayInputStream);
73+
Assertions.assertEquals(2, testIs.available());
74+
75+
byte[] bytes = new byte[2];
76+
testIs.read(bytes);
77+
Assertions.assertEquals('A', bytes[0]);
78+
Assertions.assertEquals('B', bytes[1]);
79+
Assertions.assertEquals(0, testIs.available());
80+
}
81+
}

connectors/jnh-connector/src/test/java/org/glassfish/jersey/jnh/connector/NoEntityTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.glassfish.jersey.jnh.connector;
1818

19+
import jakarta.ws.rs.core.GenericType;
1920
import org.glassfish.jersey.client.ClientConfig;
2021
import org.glassfish.jersey.logging.LoggingFeature;
2122
import org.glassfish.jersey.server.ResourceConfig;
@@ -44,6 +45,13 @@ public Response get() {
4445
@POST
4546
public void post(String entity) {
4647
}
48+
49+
@GET
50+
@Path("/success")
51+
public Response getSuccessfully() {
52+
return Response.status(Response.Status.NO_CONTENT).build();
53+
}
54+
4755
}
4856

4957
@Override
@@ -77,6 +85,32 @@ public void testGetWithClose() {
7785
}
7886
}
7987

88+
@Test
89+
public void testGetVoidWithClose() {
90+
WebTarget r = target("test");
91+
for (int i = 0; i < 5; i++) {
92+
Response cr = r.request().get();
93+
cr.close();
94+
}
95+
}
96+
97+
@Test
98+
public void testGetVoid() {
99+
WebTarget r = target("test/success");
100+
for (int i = 0; i < 5; i++) {
101+
r.request().get(void.class);
102+
}
103+
}
104+
105+
@Test
106+
public void testGetGenericVoid() {
107+
WebTarget r = target("test/success");
108+
for (int i = 0; i < 5; i++) {
109+
r.request().get(new GenericType<Void>() {
110+
});
111+
}
112+
}
113+
80114
@Test
81115
public void testPost() {
82116
WebTarget r = target("test");

0 commit comments

Comments
 (0)