Skip to content

Commit

Permalink
Workaround JNH InputStream.available() == 1 for no entity
Browse files Browse the repository at this point in the history
Signed-off-by: jansupol <[email protected]>
  • Loading branch information
jansupol committed May 4, 2023
1 parent 665655f commit 02a6a46
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;

/**
Expand Down Expand Up @@ -253,7 +255,12 @@ private ClientResponse buildClientResponse(ClientRequest request, HttpResponse<I
headers.put(headerName, entry.getValue());
}
}
clientResponse.setEntityStream(response.body());
final InputStream body = response.body();
try {
clientResponse.setEntityStream(body.available() != 1 ? body : new FirstByteCachingStream(body));
} catch (IOException ioe) {
throw new ProcessingException(ioe);
}
return clientResponse;
}

Expand Down Expand Up @@ -329,4 +336,67 @@ private SSLParameters getSslParameters(Client client) {
}
}
}

/*
* The JDK stream returns available() == 1 even when read() == -1
* This class is to prevent it.
* Otherwise, the MBR is not found for 204
* See https://github.com/eclipse-ee4j/jersey/issues/5307
*/
private static class FirstByteCachingStream extends InputStream {
private final InputStream inner;
private volatile int zero = -1; // int on zero index
private final Lock lock = new ReentrantLock();

private FirstByteCachingStream(InputStream inner) {
this.inner = inner;
}

@Override
public int read() throws IOException {
lock.lock();
final int r = zero != -1 ? zero : inner.read();
zero = -1;
lock.unlock();
return r;
}

@Override
public int read(byte[] b, int off, int len) throws IOException {
lock.lock();
int r;
if (zero != -1) {
b[off] = (byte) (zero & 0xFF);
r = inner.read(b, off + 1, len - 1);
} else {
r = inner.read(b, off, len);
}
zero = -1;
lock.unlock();
return r;

}

@Override
public int available() throws IOException {
lock.lock();
if (zero != -1) {
lock.unlock();
return 1;
}

int available = inner.available();
if (available != 1) {
return available;
}

zero = inner.read();
if (zero == -1) {
available = 0;
}
lock.unlock();
return available;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/

package org.glassfish.jersey.jnh.connector;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Constructor;

class FirstByteCachingStreamTest {
private static InputStream createFirstByteCachingStream(InputStream inner) throws Exception {
Class[] classes = JavaNetHttpConnector.class.getDeclaredClasses();
for (Class<?> clazz : classes) {
if (clazz.getName().contains("FirstByteCachingStream")) {
Constructor constructor = clazz.getDeclaredConstructor(InputStream.class);
constructor.setAccessible(true);
return (InputStream) constructor.newInstance(inner);
}
}
throw new IllegalArgumentException("JavaNetHttpConnector inner class FirstByteCachingStream not found");
}

@Test
void testNoByte() throws Exception {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(new byte[0]);
InputStream testIs = createFirstByteCachingStream(byteArrayInputStream);
Assertions.assertEquals(0, testIs.available());
}

@Test
void testOneByte() throws Exception {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(new byte[]{'A'});
InputStream testIs = createFirstByteCachingStream(byteArrayInputStream);
Assertions.assertEquals(1, testIs.available());
Assertions.assertEquals(1, testIs.available()); // idempotency
Assertions.assertEquals('A', testIs.read());
Assertions.assertEquals(0, testIs.available());
}

@Test
void testTwoBytes() throws Exception {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(new byte[]{'A', 'B'});
InputStream testIs = createFirstByteCachingStream(byteArrayInputStream);
Assertions.assertEquals(2, testIs.available());
Assertions.assertEquals(2, testIs.available()); // idempotency
Assertions.assertEquals('A', testIs.read());
Assertions.assertEquals(1, testIs.available());
Assertions.assertEquals(1, testIs.available()); // idempotency
Assertions.assertEquals('B', testIs.read());
Assertions.assertEquals(0, testIs.available());
}

@Test
void testTwoBytesReadAtOnce() throws Exception {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(new byte[]{'A', 'B'});
InputStream testIs = createFirstByteCachingStream(byteArrayInputStream);
Assertions.assertEquals(2, testIs.available());

byte[] bytes = new byte[2];
testIs.read(bytes);
Assertions.assertEquals('A', bytes[0]);
Assertions.assertEquals('B', bytes[1]);
Assertions.assertEquals(0, testIs.available());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.glassfish.jersey.jnh.connector;

import jakarta.ws.rs.core.GenericType;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.logging.LoggingFeature;
import org.glassfish.jersey.server.ResourceConfig;
Expand Down Expand Up @@ -44,6 +45,13 @@ public Response get() {
@POST
public void post(String entity) {
}

@GET
@Path("/success")
public Response getSuccessfully() {
return Response.status(Response.Status.NO_CONTENT).build();
}

}

@Override
Expand Down Expand Up @@ -77,6 +85,32 @@ public void testGetWithClose() {
}
}

@Test
public void testGetVoidWithClose() {
WebTarget r = target("test");
for (int i = 0; i < 5; i++) {
Response cr = r.request().get();
cr.close();
}
}

@Test
public void testGetVoid() {
WebTarget r = target("test/success");
for (int i = 0; i < 5; i++) {
r.request().get(void.class);
}
}

@Test
public void testGetGenericVoid() {
WebTarget r = target("test/success");
for (int i = 0; i < 5; i++) {
r.request().get(new GenericType<Void>() {
});
}
}

@Test
public void testPost() {
WebTarget r = target("test");
Expand Down

0 comments on commit 02a6a46

Please sign in to comment.