Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/*
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
* Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
Expand All @@ -16,18 +17,9 @@

package org.glassfish.jersey.netty.connector;

import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.handler.stream.ChunkedInput;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.ClientRequest;
import org.glassfish.jersey.client.spi.Connector;
import org.glassfish.jersey.client.spi.ConnectorProvider;
import org.glassfish.jersey.netty.connector.internal.NettyEntityWriter;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import javax.ws.rs.ProcessingException;
import javax.ws.rs.WebApplicationException;
Expand All @@ -38,127 +30,152 @@
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.MessageBodyWriter;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;

import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.ClientRequest;
import org.glassfish.jersey.client.spi.Connector;
import org.glassfish.jersey.client.spi.ConnectorProvider;
import org.glassfish.jersey.netty.connector.internal.NettyEntityWriter;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Bug 5837 reproducer
*/
public class ChunkedInputClosedOnErrorTest extends JerseyTest {

private static Client initClient(ConnectorProvider provider) {
ClientConfig defaultConfig = new ClientConfig();
defaultConfig.property(ClientProperties.CONNECT_TIMEOUT, 10 * 1000);
defaultConfig.property(ClientProperties.READ_TIMEOUT, 10 * 1000);
defaultConfig.connectorProvider(provider);
Client client = ClientBuilder.newBuilder()
.withConfig(defaultConfig)
.build();
return client;
}

@Override
protected Application configure() {
return new ResourceConfig();
}

@Test
public void testChunkedInputNotStuckedTimes() throws InterruptedException {
for (int i = 0; i != 10; i++) {
boolean ret = testChunkedInputNotStucked();
Assertions.assertTrue(ret, "JerseyChunkedInput was not closed on error");
public void testChunkedInputNotStuckedTimes() throws Exception {
long timeout = System.currentTimeMillis() + 100;
int counter = 0;
while (System.currentTimeMillis() < timeout || counter < 300) {
counter++;
TestNettyConnectorProvider provider = new TestNettyConnectorProvider();
Client client = provider.initClient();
ProcessingException processingException = assertThrows(ProcessingException.class,
() -> client.register(new MultipartWriter()).target(target().getUri()).request()
.post(Entity.entity(new MultipartWriter(), MediaType.MULTIPART_FORM_DATA_TYPE)));
provider.waitForWriterSet();
assertAll("Iteration: " + counter,
() -> assertInstanceOf(IllegalArgumentException.class, processingException.getCause()),
() -> assertEquals("TestException", processingException.getCause().getMessage()),
() -> assertTrue(provider.isEndOfInput(), "JerseyChunkedInput was not closed on error")
);
}
}

public boolean testChunkedInputNotStucked() throws InterruptedException {
final AtomicReference<NettyEntityWriter> writer = new AtomicReference<>();
final CountDownLatch writerSetLatch = new CountDownLatch(1);
final CountDownLatch flushLatch = new CountDownLatch(1);
ConnectorProvider provider = new ConnectorProvider() {
@Override
public Connector getConnector(Client client, Configuration runtimeConfig) {
return new NettyConnector(client, NettyConnectorProvider.config().rw()) {
@Override
NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel,
NettyConnectorProvider.Config.RW config) {
writer.set(super.nettyEntityWriter(clientRequest, channel, config));
writerSetLatch.countDown();
return new NettyEntityWriter() {
private boolean slept = false;

@Override
public void write(Object object) {
writer.get().write(object);
}

@Override
public void writeAndFlush(Object object) {
writer.get().writeAndFlush(object);
}
private static class TestNettyConnectorProvider implements ConnectorProvider {

@Override
public void flush() throws IOException {
writer.get().flush();
flushLatch.countDown();
}
private final AtomicReference<NettyEntityWriter> writer = new AtomicReference<>();
private final CountDownLatch writerSetLatch = new CountDownLatch(1);
private final CountDownLatch flushLatch = new CountDownLatch(1);

@Override
public ChunkedInput getChunkedInput() {
for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
// caught from catch block in executorService.execute(new Runnable() {
// "sleep" to simulate race condition
if (element.getClassName().contains("NettyConnector")
&& element.getMethodName().equals("run")) {
try {
flushLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
return writer.get().getChunkedInput();
}

@Override
public OutputStream getOutputStream() {
return writer.get().getOutputStream();
}
Client initClient() {
ClientConfig defaultConfig = new ClientConfig();
defaultConfig.property(ClientProperties.CONNECT_TIMEOUT, 1_000);
defaultConfig.property(ClientProperties.READ_TIMEOUT, 10_000);
defaultConfig.connectorProvider(this);
return ClientBuilder.newBuilder().withConfig(defaultConfig).build();
}

@Override
public long getLength() {
return writer.get().getLength();
}
void waitForWriterSet() throws InterruptedException {
writerSetLatch.await();
}

@Override
public Type getType() {
return writer.get().getType();
}
};
}
};
boolean isEndOfInput() throws Exception {
// getChunkedInput blocks and waits for the flush call, but that doesn't mean
// that the ChunkedInput is closed, so we give it a bit more time.
ChunkedInput<ByteBuf> chunkedInput = writer.get().getChunkedInput();
long timeout = System.currentTimeMillis() + 100;
while (!chunkedInput.isEndOfInput() && System.currentTimeMillis() < timeout) {
Thread.yield();
}
};

Client client = initClient(provider);
try {
Response r = client
.register(new MultipartWriter())
.target(target().getUri()).request()
.post(Entity.entity(new MultipartWriter(), MediaType.MULTIPART_FORM_DATA_TYPE));
} catch (ProcessingException expected) {

return chunkedInput.isEndOfInput();
}
writerSetLatch.await();
try {
return writer.get().getChunkedInput().isEndOfInput();
} catch (Exception e) {
throw new RuntimeException(e);

@Override
public Connector getConnector(Client client, Configuration runtimeConfig) {
return new NettyConnector(client, NettyConnectorProvider.config().rw()) {

@Override
NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel,
NettyConnectorProvider.Config.RW config) {
writer.set(super.nettyEntityWriter(clientRequest, channel, config));
writerSetLatch.countDown();
return new NettyEntityWriter() {

@Override
public void write(Object object) {
writer.get().write(object);
}

@Override
public void writeAndFlush(Object object) {
writer.get().writeAndFlush(object);
}

@Override
public void flush() throws IOException {
writer.get().flush();
flushLatch.countDown();
}

@Override
public ChunkedInput<ByteBuf> getChunkedInput() {
for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
// caught from catch block in executorService.execute(new Runnable()
// {
// "sleep" to simulate race condition
if (element.getClassName().contains("NettyConnector")
&& element.getMethodName().equals("run")) {
try {
flushLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
return writer.get().getChunkedInput();
}

@Override
public OutputStream getOutputStream() {
return writer.get().getOutputStream();
}

@Override
public long getLength() {
return writer.get().getLength();
}

@Override
public Type getType() {
return writer.get().getType();
}
};
}
};
}
}

Expand All @@ -170,11 +187,10 @@ public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotat
}

@Override
public void writeTo(Object object, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException,
WebApplicationException {
public void writeTo(Object object, Class<?> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
throws IOException, WebApplicationException {
throw new IllegalArgumentException("TestException");
}
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/*
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
* Copyright (c) 2014, 2022 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
Expand All @@ -21,6 +22,8 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.junit.jupiter.api.DynamicContainer;
import org.junit.jupiter.api.DynamicTest;
Expand All @@ -32,6 +35,7 @@
* @author Michal Gajdos
*/
public final class TestHelper {
private static final Logger LOG = Logger.getLogger(TestHelper.class.getName());

/**
* Create a human readable string from given URI. This method replaces {@code 0} port (start container at first available
Expand Down Expand Up @@ -65,6 +69,7 @@ public static DynamicContainer toTestContainer(Object test, String displayName)
for (Method method : testMethods) {
children.add(DynamicTest.dynamicTest(method.getName(), () -> {
try {
LOG.log(Level.INFO, "Invoking test " + displayName + "." + method.getName());
for (Method beforeEachMethod : beforeEachMethods) {
beforeEachMethod.invoke(test);
}
Expand Down
Loading