merge of the current 2.x into the 3.0
diff --git a/NOTICE.md b/NOTICE.md
index e9981e6..b245c38 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -95,7 +95,7 @@
* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS
* Copyright: Eric Rowell
-org.objectweb.asm Version 9.7.1
+org.objectweb.asm Version 9.8
* License: Modified BSD (https://asm.ow2.io/license.html)
* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java
index 949003f..7f5bb9b 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java
@@ -91,7 +91,7 @@
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
- notifyResponse();
+ notifyResponse(ctx);
}
@Override
@@ -107,7 +107,7 @@
}
}
- protected void notifyResponse() {
+ protected void notifyResponse(ChannelHandlerContext ctx) {
if (jerseyResponse != null) {
ClientResponse cr = jerseyResponse;
jerseyResponse = null;
@@ -146,6 +146,7 @@
} else {
ClientRequest newReq = new ClientRequest(jerseyRequest);
newReq.setUri(newUri);
+ ctx.close();
if (redirectController.prepareRedirect(newReq, cr)) {
final NettyConnector newConnector = new NettyConnector(newReq.getClient());
newConnector.execute(newReq, redirectUriHistory, new CompletableFuture<ClientResponse>() {
@@ -227,7 +228,7 @@
if (msg instanceof LastHttpContent) {
responseDone.complete(null);
- notifyResponse();
+ notifyResponse(ctx);
}
}
}
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java
index 8bceac6..6bee4d3 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2025 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
@@ -16,111 +16,133 @@
package org.glassfish.jersey.netty.connector;
-import io.netty.channel.Channel;
-import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
-import io.netty.handler.codec.http.DefaultFullHttpRequest;
-import io.netty.handler.codec.http.HttpHeaderNames;
-import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
-import io.netty.handler.codec.http.HttpUtil;
-import org.glassfish.jersey.client.ClientRequest;
+import io.netty.handler.codec.http.LastHttpContent;
import jakarta.ws.rs.ProcessingException;
+import java.io.IOException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeoutException;
public class JerseyExpectContinueHandler extends ChannelInboundHandlerAdapter {
- private boolean isExpected;
+ private ExpectationState currentState = ExpectationState.IDLE;
- private static final List<HttpResponseStatus> statusesToBeConsidered = Arrays.asList(HttpResponseStatus.CONTINUE,
- HttpResponseStatus.UNAUTHORIZED, HttpResponseStatus.EXPECTATION_FAILED,
- HttpResponseStatus.METHOD_NOT_ALLOWED, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE);
+ private static final List<HttpResponseStatus> finalErrorStatuses = Arrays.asList(HttpResponseStatus.UNAUTHORIZED,
+ HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE);
+ private static final List<HttpResponseStatus> reSendErrorStatuses = Arrays.asList(
+ HttpResponseStatus.METHOD_NOT_ALLOWED,
+ HttpResponseStatus.EXPECTATION_FAILED);
- private CompletableFuture<HttpResponseStatus> expectedFuture = new CompletableFuture<>();
+ private static final List<HttpResponseStatus> statusesToBeConsidered = new ArrayList<>(reSendErrorStatuses);
+
+ static {
+ statusesToBeConsidered.addAll(finalErrorStatuses);
+ statusesToBeConsidered.add(HttpResponseStatus.CONTINUE);
+ }
+
+ private HttpResponseStatus status = null;
+
+ private CountDownLatch latch = null;
+
+ private boolean propagateLastMessage = false;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
- if (isExpected && msg instanceof HttpResponse) {
- final HttpResponse response = (HttpResponse) msg;
- if (statusesToBeConsidered.contains(response.status())) {
- expectedFuture.complete(response.status());
- }
- if (!HttpResponseStatus.CONTINUE.equals(response.status())) {
+
+ if (checkExpectResponse(msg) || checkInvalidExpect(msg)) {
+ currentState = ExpectationState.AWAITING;
+ }
+ switch (currentState) {
+ case AWAITING:
+ final HttpResponse response = (HttpResponse) msg;
+ status = response.status();
+ boolean handshakeDone = processErrorStatuses(status) || msg instanceof FullHttpMessage;
+ currentState = (handshakeDone) ? ExpectationState.IDLE : ExpectationState.FINISHING;
+ processLatch();
+ return;
+ case FINISHING:
+ if (msg instanceof LastHttpContent) {
+ currentState = ExpectationState.IDLE;
+ if (propagateLastMessage) {
+ propagateLastMessage = false;
+ ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
+ }
+ }
+ return;
+ default:
ctx.fireChannelRead(msg); //bypass the message to the next handler in line
- } else {
- ctx.pipeline().remove(JerseyExpectContinueHandler.class);
- }
- } else {
- if (!isExpected) {
- ctx.pipeline().remove(JerseyExpectContinueHandler.class);
- }
- ctx.fireChannelRead(msg); //bypass the message to the next handler in line
}
}
- CompletableFuture<HttpResponseStatus> processExpect100ContinueRequest(HttpRequest nettyRequest,
- ClientRequest jerseyRequest,
- Channel ch,
- Integer timeout)
- throws InterruptedException, ExecutionException, TimeoutException {
- //check for 100-Continue presence/availability
- final Expect100ContinueConnectorExtension expect100ContinueExtension
- = new Expect100ContinueConnectorExtension();
-
- final DefaultFullHttpRequest nettyRequestHeaders =
- new DefaultFullHttpRequest(nettyRequest.protocolVersion(), nettyRequest.method(), nettyRequest.uri());
- nettyRequestHeaders.headers().setAll(nettyRequest.headers());
-
- if (!nettyRequestHeaders.headers().contains(HttpHeaderNames.HOST)) {
- nettyRequestHeaders.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost());
+ private boolean checkExpectResponse(Object msg) {
+ if (currentState == ExpectationState.IDLE && latch != null && msg instanceof HttpResponse) {
+ return statusesToBeConsidered.contains(((HttpResponse) msg).status());
}
-
- //If Expect:100-continue feature is enabled and client supports it, the nettyRequestHeaders will be
- //enriched with the 'Expect:100-continue' header.
- expect100ContinueExtension.invoke(jerseyRequest, nettyRequestHeaders);
-
- final ChannelFuture expect100ContinueFuture = (HttpUtil.is100ContinueExpected(nettyRequestHeaders))
- // Send only head of the HTTP request enriched with Expect:100-continue header.
- ? ch.writeAndFlush(nettyRequestHeaders)
- // Expect:100-Continue either is not supported or is turned off
- : null;
- isExpected = expect100ContinueFuture != null;
- if (!isExpected) {
- ch.pipeline().remove(JerseyExpectContinueHandler.class);
- } else {
- final HttpResponseStatus status = expectedFuture
- .get(timeout, TimeUnit.MILLISECONDS);
-
- processExpectationStatus(status);
- }
- return expectedFuture;
+ return false;
}
- private void processExpectationStatus(HttpResponseStatus status)
- throws TimeoutException {
+ private boolean checkInvalidExpect(Object msg) {
+ return (ExpectationState.IDLE.equals(currentState)
+ && msg instanceof HttpResponse
+ && (HttpResponseStatus.CONTINUE.equals(((HttpResponse) msg).status())
+ || reSendErrorStatuses.contains(((HttpResponse) msg).status()))
+ );
+ }
+
+ boolean processErrorStatuses(HttpResponseStatus status) {
+ if (reSendErrorStatuses.contains(status)) {
+ propagateLastMessage = true;
+ }
+ return (finalErrorStatuses.contains(status));
+ }
+
+ void processExpectationStatus()
+ throws TimeoutException, IOException {
+ if (status == null) {
+ throw new TimeoutException(); // continue without expectations
+ }
if (!statusesToBeConsidered.contains(status)) {
throw new ProcessingException(LocalizationMessages
.UNEXPECTED_VALUE_FOR_EXPECT_100_CONTINUE_STATUSES(status.code()), null);
}
- if (!expectedFuture.isDone() || HttpResponseStatus.EXPECTATION_FAILED.equals(status)) {
- isExpected = false;
- throw new TimeoutException(); // continue without expectations
+
+ if (finalErrorStatuses.contains(status)) {
+ throw new IOException(LocalizationMessages
+ .EXPECT_100_CONTINUE_FAILED_REQUEST_FAILED(), null);
}
- if (!HttpResponseStatus.CONTINUE.equals(status)) {
- throw new ProcessingException(LocalizationMessages
- .UNEXPECTED_VALUE_FOR_EXPECT_100_CONTINUE_STATUSES(status.code()), null);
+
+ if (reSendErrorStatuses.contains(status)) {
+ throw new TimeoutException(LocalizationMessages
+ .EXPECT_100_CONTINUE_FAILED_REQUEST_SHOULD_BE_RESENT()); // Re-send request without expectations
+ }
+
+ }
+
+ void resetHandler() {
+ latch = null;
+ }
+
+ void attachCountDownLatch(CountDownLatch latch) {
+ this.latch = latch;
+ }
+
+ private void processLatch() {
+ if (latch != null) {
+ latch.countDown();
}
}
- boolean isExpected() {
- return isExpected;
+ private enum ExpectationState {
+ AWAITING,
+ FINISHING,
+ IDLE
}
-}
\ No newline at end of file
+}
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
index a4622b8..a65560c 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
@@ -17,9 +17,7 @@
package org.glassfish.jersey.netty.connector;
import java.io.IOException;
-import java.io.InterruptedIOException;
import java.io.OutputStream;
-import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
@@ -34,7 +32,6 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@@ -70,6 +67,7 @@
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.proxy.HttpProxyHandler;
import io.netty.handler.proxy.ProxyHandler;
import io.netty.handler.ssl.ApplicationProtocolConfig;
@@ -257,6 +255,8 @@
}
}
+ final JerseyExpectContinueHandler expect100ContinueHandler = new JerseyExpectContinueHandler();
+
if (chan == null) {
Integer connectTimeout = jerseyRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, 0);
Bootstrap b = new Bootstrap();
@@ -329,8 +329,8 @@
final Integer maxInitialLineLength = ClientProperties.getValue(config.getProperties(),
NettyClientProperties.MAX_INITIAL_LINE_LENGTH,
NettyClientProperties.DEFAULT_INITIAL_LINE_LENGTH);
-
p.addLast(new HttpClientCodec(maxInitialLineLength, maxHeaderSize, maxChunkSize));
+ p.addLast(EXPECT_100_CONTINUE_HANDLER, expect100ContinueHandler);
p.addLast(new ChunkedWriteHandler());
p.addLast(new HttpContentDecompressor());
}
@@ -359,11 +359,10 @@
final Channel ch = chan;
JerseyClientHandler clientHandler =
new JerseyClientHandler(jerseyRequest, responseAvailable, responseDone, redirectUriHistory, this);
- final JerseyExpectContinueHandler expect100ContinueHandler = new JerseyExpectContinueHandler();
+
// read timeout makes sense really as an inactivity timeout
ch.pipeline().addLast(READ_TIMEOUT_HANDLER,
new IdleStateHandler(0, 0, timeout, TimeUnit.MILLISECONDS));
- ch.pipeline().addLast(EXPECT_100_CONTINUE_HANDLER, expect100ContinueHandler);
ch.pipeline().addLast(REQUEST_HANDLER, clientHandler);
responseDone.whenComplete((_r, th) -> {
@@ -446,22 +445,11 @@
// // Set later after the entity is "written"
// break;
}
- try {
- expect100ContinueHandler.processExpect100ContinueRequest(nettyRequest, jerseyRequest,
- ch, expect100ContinueTimeout);
- } catch (ExecutionException e) {
- responseDone.completeExceptionally(e);
- } catch (TimeoutException e) {
- //Expect:100-continue allows timeouts by the spec
- //just removing the pipeline from processing
- if (ch.pipeline().context(JerseyExpectContinueHandler.class) != null) {
- ch.pipeline().remove(EXPECT_100_CONTINUE_HANDLER);
- }
- }
final CountDownLatch headersSet = new CountDownLatch(1);
final CountDownLatch contentLengthSet = new CountDownLatch(1);
+
jerseyRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() {
@Override
public OutputStream getOutputStream(int contentLength) throws IOException {
@@ -486,7 +474,6 @@
try {
jerseyRequest.writeEntity();
-
if (entityWriter.getType() == NettyEntityWriter.Type.DELAYED) {
nettyRequest.headers().set(HttpHeaderNames.CONTENT_LENGTH, entityWriter.getLength());
contentLengthSet.countDown();
@@ -506,12 +493,36 @@
});
headersSet.await();
- if (!expect100ContinueHandler.isExpected()) {
- // Send the HTTP request. Expect:100-continue processing is not applicable
- // in this case.
+ new Expect100ContinueConnectorExtension().invoke(jerseyRequest, nettyRequest);
+
+ boolean continueExpected = HttpUtil.is100ContinueExpected(nettyRequest);
+ boolean expectationsFailed = false;
+
+ if (continueExpected) {
+ final CountDownLatch expect100ContinueLatch = new CountDownLatch(1);
+ expect100ContinueHandler.attachCountDownLatch(expect100ContinueLatch);
+ //send expect request, sync and wait till either response or timeout received
entityWriter.writeAndFlush(nettyRequest);
+ expect100ContinueLatch.await(expect100ContinueTimeout, TimeUnit.MILLISECONDS);
+ try {
+ expect100ContinueHandler.processExpectationStatus();
+ } catch (TimeoutException e) {
+ //Expect:100-continue allows timeouts by the spec
+ //so, send request directly without Expect header.
+ expectationsFailed = true;
+ } finally {
+ //restore request and handler to the original state.
+ HttpUtil.set100ContinueExpected(nettyRequest, false);
+ expect100ContinueHandler.resetHandler();
+ }
}
+ if (!continueExpected || expectationsFailed) {
+ if (expectationsFailed) {
+ ch.pipeline().writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT).sync();
+ }
+ entityWriter.writeAndFlush(nettyRequest);
+ }
if (HttpUtil.isTransferEncodingChunked(nettyRequest)) {
entityWriter.write(new HttpChunkedInput(entityWriter.getChunkedInput()));
} else {
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/JerseyChunkedInput.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/JerseyChunkedInput.java
index 4ffe52e..baa9118 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/JerseyChunkedInput.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/JerseyChunkedInput.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2025 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
@@ -68,7 +68,8 @@
ByteBuffer peek = queue.peek();
if ((peek != null && peek == VOID)) {
- queue.remove(); // VOID from the top.
+ //required for JDK 11 and netty.version = 4.1.121.Final
+ queue.poll(); // VOID from the top.
open = false;
removeCloseListener();
return true;
diff --git a/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties b/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties
index 7d6f9fc..dd5de49 100644
--- a/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties
+++ b/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2016, 2023 Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2016, 2025 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
@@ -23,3 +23,5 @@
redirect.infinite.loop="Infinite loop in chained redirects detected."
redirect.limit.reached="Max chained redirect limit ({0}) exceeded."
unexpected.value.for.expect.100.continue.statuses=Unexpected value: ("{0}").
+expect.100.continue.failed.request.should.be.resent=Expect 100-continue failed. Request should be resent.
+expect.100.continue.failed.request.failed=Expect 100-continue failed. Request failed.
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebComponent.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebComponent.java
index 6e795f1..7ba0f51 100644
--- a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebComponent.java
+++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebComponent.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 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
@@ -38,6 +38,7 @@
import java.util.logging.Logger;
import java.util.stream.Collectors;
+import jakarta.servlet.ServletInputStream;
import jakarta.ws.rs.RuntimeType;
import jakarta.ws.rs.core.Form;
import jakarta.ws.rs.core.GenericType;
@@ -425,13 +426,18 @@
try {
requestContext.setEntityStream(new InputStreamWrapper() {
+
+ private ServletInputStream wrappedStream;
@Override
protected InputStream getWrapped() {
- try {
- return servletRequest.getInputStream();
- } catch (IOException e) {
- throw new UncheckedIOException(e);
+ if (wrappedStream == null) {
+ try {
+ wrappedStream = servletRequest.getInputStream();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
}
+ return wrappedStream;
}
});
} catch (UncheckedIOException e) {
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java
index 5188a1a..c3c6c1f 100644
--- a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java
+++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 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
@@ -116,6 +116,10 @@
@Override
public OutputStream writeResponseStatusAndHeaders(final long contentLength, final ContainerResponse responseContext)
throws ContainerException {
+ if (asyncExt.isCompleted()) {
+ return null;
+ }
+
this.responseContext.complete(responseContext);
// first set the content length, so that if headers have an explicit value, it takes precedence over this one
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/AsyncContextDelegate.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/AsyncContextDelegate.java
index 5825375..2652e62 100644
--- a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/AsyncContextDelegate.java
+++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/AsyncContextDelegate.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 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
@@ -42,4 +42,17 @@
* Invoked upon a response writing completion when the response write is either committed or canceled.
*/
public void complete();
+
+ /**
+ * <p>
+ * Return {@code true} when the AsyncContext is completed, such as when {@link #complete()} has been called.
+ * </p>
+ * <p>
+ * For compatibility, the default is {@code false}.
+ * </p>
+ * @return {@code true} when the AsyncContext is completed.
+ */
+ public default boolean isCompleted() {
+ return false;
+ }
}
diff --git a/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/async/AsyncContextDelegateProviderImpl.java b/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/async/AsyncContextDelegateProviderImpl.java
index efe248e..89fa99a 100644
--- a/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/async/AsyncContextDelegateProviderImpl.java
+++ b/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/async/AsyncContextDelegateProviderImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 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
@@ -16,12 +16,15 @@
package org.glassfish.jersey.servlet.async;
+import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import jakarta.servlet.AsyncContext;
+import jakarta.servlet.AsyncEvent;
+import jakarta.servlet.AsyncListener;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -70,7 +73,32 @@
public void suspend() throws IllegalStateException {
// Suspend only if not completed and not suspended before.
if (!completed.get() && asyncContextRef.get() == null) {
- asyncContextRef.set(getAsyncContext());
+ final AsyncContext asyncContext = getAsyncContext();
+ asyncContext.addListener(new CompletedAsyncContextListener());
+ asyncContextRef.set(asyncContext);
+ }
+ }
+
+ private class CompletedAsyncContextListener implements AsyncListener {
+
+ @Override
+ public void onComplete(AsyncEvent event) throws IOException {
+ complete();
+ }
+
+ @Override
+ public void onTimeout(AsyncEvent event) throws IOException {
+
+ }
+
+ @Override
+ public void onError(AsyncEvent event) throws IOException {
+ complete();
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent event) throws IOException {
+
}
}
@@ -102,5 +130,10 @@
asyncContext.complete();
}
}
+
+ @Override
+ public boolean isCompleted() {
+ return completed.get();
+ }
}
}
diff --git a/containers/jersey-servlet/src/test/java/org/glassfish/jersey/servlet/async/AsyncContextClosedTest.java b/containers/jersey-servlet/src/test/java/org/glassfish/jersey/servlet/async/AsyncContextClosedTest.java
new file mode 100644
index 0000000..220d832
--- /dev/null
+++ b/containers/jersey-servlet/src/test/java/org/glassfish/jersey/servlet/async/AsyncContextClosedTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2025 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.servlet.async;
+
+import org.glassfish.jersey.server.spi.ContainerResponseWriter;
+import org.glassfish.jersey.servlet.internal.ResponseWriter;
+import org.junit.jupiter.api.Test;
+
+import jakarta.servlet.AsyncContext;
+import jakarta.servlet.AsyncListener;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+public class AsyncContextClosedTest {
+ @Test
+ public void testClosedAsyncContext() {
+ List<AsyncListener> asyncListeners = new ArrayList<>(1);
+ AsyncContext async = (AsyncContext) Proxy.newProxyInstance(getClass().getClassLoader(),
+ new Class[]{AsyncContext.class}, new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ switch (method.getName()) {
+ case "addListener":
+ asyncListeners.add((AsyncListener) args[0]);
+ break;
+ case "complete":
+ asyncListeners.forEach((asyncListener -> {
+ try {
+ asyncListener.onComplete(null);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }));
+ }
+ return null;
+ }
+ });
+
+ HttpServletRequest request = (HttpServletRequest) Proxy.newProxyInstance(getClass().getClassLoader(),
+ new Class[]{HttpServletRequest.class}, new InvocationHandler() {
+ boolean asyncStarted = false;
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ switch (method.getName()) {
+ case "isAsyncStarted":
+ return asyncStarted;
+ case "startAsync":
+ asyncStarted = true;
+ return async;
+ case "getAsyncContext":
+ return async;
+
+ }
+ return null;
+ }
+ });
+
+ HttpServletResponse response = (HttpServletResponse) Proxy.newProxyInstance(getClass().getClassLoader(),
+ new Class[]{HttpServletResponse.class}, new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ return null;
+ }
+ });
+
+ // Create writer
+ ResponseWriter writer = new ResponseWriter(false, false, response,
+ new AsyncContextDelegateProviderImpl().createDelegate(request, response),
+ Executors.newSingleThreadScheduledExecutor());
+ writer.suspend(10, TimeUnit.SECONDS, new ContainerResponseWriter.TimeoutHandler() {
+ @Override
+ public void onTimeout(ContainerResponseWriter responseWriter) {
+ throw new IllegalStateException();
+ }
+ });
+ // Simulate completion by the Servlet Container;
+ request.getAsyncContext().complete();
+ // Check write is ignored
+ writer.writeResponseStatusAndHeaders(10, null);
+ }
+}
diff --git a/core-client/pom.xml b/core-client/pom.xml
index fefc684..f7eac8c 100644
--- a/core-client/pom.xml
+++ b/core-client/pom.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
- Copyright (c) 2011, 2024 Oracle and/or its affiliates. All rights reserved.
+ Copyright (c) 2011, 2025 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
@@ -144,6 +144,12 @@
</dependency>
<dependency>
+ <groupId>jakarta.xml.bind</groupId>
+ <artifactId>jakarta.xml.bind-api</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>${project.version}</version>
@@ -153,6 +159,19 @@
<profiles>
<profile>
+ <id>mockito_jdk_11</id>
+ <activation>
+ <jdk>11</jdk>
+ </activation>
+ <dependencies>
+ <dependency>
+ <groupId>jakarta.xml.bind</groupId>
+ <artifactId>jakarta.xml.bind-api</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ </profile>
+ <profile>
<id>sonar</id>
<build>
<plugins>
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientResponse.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientResponse.java
index 9491741..35fcef5 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/ClientResponse.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientResponse.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 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
@@ -22,6 +22,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
@@ -77,18 +78,19 @@
ByteArrayOutputStream baos = new ByteArrayOutputStream();
OutputStream stream = null;
try {
- try {
- stream = requestContext.getWorkers().writeTo(
- entity, entity.getClass(), null, null, response.getMediaType(),
- response.getMetadata(), requestContext.getPropertiesDelegate(), baos,
- Collections.<WriterInterceptor>emptyList());
- } finally {
- if (stream != null) {
- stream.close();
- }
- }
+ final Type t = response instanceof OutboundJaxrsResponse
+ ? ((OutboundJaxrsResponse) response).getContext().getEntityType()
+ : null;
+ stream = requestContext.getWorkers().writeTo(
+ entity, entity.getClass(), t, null, response.getMediaType(),
+ response.getMetadata(), requestContext.getPropertiesDelegate(), baos,
+ Collections.<WriterInterceptor>emptyList());
} catch (IOException e) {
// ignore
+ } finally {
+ if (stream != null) {
+ stream.close();
+ }
}
byteArrayInputStream = new ByteArrayInputStream(baos.toByteArray());
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/authentication/BasicAuthenticator.java b/core-client/src/main/java/org/glassfish/jersey/client/authentication/BasicAuthenticator.java
index 9637066..d38fc2b 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/authentication/BasicAuthenticator.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/authentication/BasicAuthenticator.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2013, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2025 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
@@ -17,8 +17,8 @@
package org.glassfish.jersey.client.authentication;
import java.util.Base64;
+import java.util.List;
import java.util.Locale;
-import java.util.logging.Level;
import java.util.logging.Logger;
import jakarta.ws.rs.client.ClientRequestContext;
@@ -96,20 +96,22 @@
* @throws ResponseAuthenticationException in case that basic credentials missing or are in invalid format
*/
public boolean filterResponseAndAuthenticate(ClientRequestContext request, ClientResponseContext response) {
- final String authenticate = response.getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE);
- if (authenticate != null && authenticate.trim().toUpperCase(Locale.ROOT).startsWith("BASIC")) {
- HttpAuthenticationFilter.Credentials credentials = HttpAuthenticationFilter
- .getCredentials(request, defaultCredentials, HttpAuthenticationFilter.Type.BASIC);
-
- if (credentials == null) {
- if (response.hasEntity()) {
- AuthenticationUtil.discardInputAndClose(response.getEntityStream());
- }
- throw new ResponseAuthenticationException(null, LocalizationMessages.AUTHENTICATION_CREDENTIALS_MISSING_BASIC());
- }
-
- return HttpAuthenticationFilter.repeatRequest(request, response, calculateAuthentication(credentials));
+ final List<String> authHeaders = response.getHeaders().get(HttpHeaders.WWW_AUTHENTICATE);
+ if (authHeaders == null || authHeaders.size() == 0 || authHeaders.stream()
+ .noneMatch(h -> h != null && h.toUpperCase(Locale.ROOT).startsWith("BASIC"))) {
+ return false;
}
- return false;
+
+ HttpAuthenticationFilter.Credentials credentials = HttpAuthenticationFilter
+ .getCredentials(request, defaultCredentials, HttpAuthenticationFilter.Type.BASIC);
+
+ if (credentials == null) {
+ if (response.hasEntity()) {
+ AuthenticationUtil.discardInputAndClose(response.getEntityStream());
+ }
+ throw new ResponseAuthenticationException(null, LocalizationMessages.AUTHENTICATION_CREDENTIALS_MISSING_BASIC());
+ }
+
+ return HttpAuthenticationFilter.repeatRequest(request, response, calculateAuthentication(credentials));
}
}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java
index 49fc03c..212e83d 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2025 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
@@ -20,12 +20,14 @@
import org.glassfish.jersey.client.ClientRequest;
import org.glassfish.jersey.http.HttpHeaders;
import org.glassfish.jersey.internal.PropertiesResolver;
+import org.glassfish.jersey.internal.guava.InetAddresses;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import jakarta.ws.rs.core.Configuration;
import jakarta.ws.rs.core.UriBuilder;
+import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
@@ -233,7 +235,9 @@
String host = uri.getHost();
try {
InetAddress ip = InetAddress.getByName(host);
- return UriBuilder.fromUri(uri).host(ip.getHostAddress()).build();
+ // ipv6 is expected in square brackets in UriBuilder#host()
+ final String hostAddress = ip instanceof Inet6Address ? '[' + ip.getHostAddress() + ']' : ip.getHostAddress();
+ return UriBuilder.fromUri(uri).host(hostAddress).build();
} catch (UnknownHostException e) {
return uri;
}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java
index cb1d937..10cdfac 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2025 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
@@ -61,7 +61,10 @@
static Optional<SniConfigurator> createWhenHostHeader(URI hostUri, String sniHost, boolean whenDiffer) {
final String trimmedHeader;
if (sniHost != null) {
- int index = sniHost.indexOf(':'); // RFC 7230 Host = uri-host [ ":" port ] ;
+ int index = sniHost.lastIndexOf(':'); // RFC 7230 Host = uri-host [ ":" port ] ;
+ if (sniHost.indexOf(']', index) != -1) {
+ index = -1; // beware of ipv6 [:1] without port
+ }
final String trimmedHeader0 = index != -1 ? sniHost.substring(0, index).trim() : sniHost.trim();
trimmedHeader = trimmedHeader0.isEmpty() ? sniHost : trimmedHeader0;
} else {
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java b/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java
new file mode 100644
index 0000000..caffa4d
--- /dev/null
+++ b/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2025 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.client;
+
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.core.GenericEntity;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.MessageBodyWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+//import static java.nio.charset.StandardCharsets;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class AbortTest {
+ private static final String TEXT_CSV = "text/csv";
+ private static final String EXPECTED_CSV = "hello;goodbye\nsalutations;farewell";
+ private static final List<List<String>> CSV_LIST = Arrays.asList(
+ Arrays.asList("hello", "goodbye"),
+ Arrays.asList("salutations", "farewell")
+ );
+
+ @Test
+ void testAbortWithGenericEntity() {
+ Client client = ClientBuilder.newBuilder()
+ .register(AbortRequestFilter.class)
+ .register(CsvWriter.class)
+ .build();
+ String csvString = client.target("http://localhost:8080")
+ .request(TEXT_CSV)
+ .get(String.class);
+ assertEquals(EXPECTED_CSV, csvString);
+ client.close();
+ }
+
+ public static class AbortRequestFilter implements ClientRequestFilter {
+
+ @Override
+ public void filter(ClientRequestContext requestContext) {
+ requestContext.abortWith(Response.ok(new GenericEntity<List<List<String>>>(CSV_LIST) {
+ }).type(TEXT_CSV).build());
+ }
+ }
+
+ @Produces(TEXT_CSV)
+ public static class CsvWriter implements MessageBodyWriter<List<List<String>>> {
+
+ @Override
+ public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+ System.out.println(genericType.getTypeName());
+ return List.class.isAssignableFrom(type) && genericType instanceof ParameterizedType
+ && ((ParameterizedType) genericType).getActualTypeArguments()[0] instanceof ParameterizedType
+ && String.class.equals(((ParameterizedType) ((ParameterizedType) genericType).getActualTypeArguments()[0])
+ .getActualTypeArguments()[0]);
+ }
+
+ @Override
+ public void writeTo(List<List<String>> csvList, Class<?> type, Type genericType, Annotation[] annotations,
+ MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
+ throws IOException, WebApplicationException {
+ List<String> rows = new ArrayList<>();
+ for (List<String> row : csvList) {
+ rows.add(String.join(";", row));
+ }
+ String csv = String.join("\n", rows);
+
+ entityStream.write(csv.getBytes(StandardCharsets.UTF_8));
+ entityStream.flush();
+ }
+ }
+
+}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/authentication/BasicAuthenticatorTest.java b/core-client/src/test/java/org/glassfish/jersey/client/authentication/BasicAuthenticatorTest.java
new file mode 100644
index 0000000..ad5b721
--- /dev/null
+++ b/core-client/src/test/java/org/glassfish/jersey/client/authentication/BasicAuthenticatorTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2025 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.client.authentication;
+
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientResponseContext;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MultivaluedMap;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class BasicAuthenticatorTest {
+
+ @Test
+ void filterResponseAndAuthenticateNoAuthHeadersTest() {
+ final BasicAuthenticator authenticator
+ = new BasicAuthenticator(new HttpAuthenticationFilter.Credentials("foo", "bar"));
+ final ClientRequestContext request = mock(ClientRequestContext.class);
+ final ClientResponseContext response = mock(ClientResponseContext.class);
+
+ when(response.getHeaders()).thenReturn(mock(MultivaluedMap.class));
+
+ assertFalse(authenticator.filterResponseAndAuthenticate(request, response));
+ }
+
+ @Test
+ void filterResponseAndAuthenticateAuthHeaderNotBasicTest() {
+ final BasicAuthenticator authenticator
+ = new BasicAuthenticator(new HttpAuthenticationFilter.Credentials("foo", "bar"));
+ final ClientRequestContext request = mock(ClientRequestContext.class);
+ final ClientResponseContext response = mock(ClientResponseContext.class);
+
+ final MultivaluedMap<String, String> headers = mock(MultivaluedMap.class);
+ when(response.getHeaders()).thenReturn(headers);
+ when(headers.get(HttpHeaders.WWW_AUTHENTICATE)).thenReturn(Collections.singletonList("Digest realm=\"test\""));
+
+ assertFalse(authenticator.filterResponseAndAuthenticate(request, response));
+ }
+
+ @Test
+ void filterResponseAndAuthenticateEmptyListTest() {
+ final BasicAuthenticator authenticator
+ = new BasicAuthenticator(new HttpAuthenticationFilter.Credentials("foo", "bar"));
+ final ClientRequestContext request = mock(ClientRequestContext.class);
+ final ClientResponseContext response = mock(ClientResponseContext.class);
+
+ final MultivaluedMap<String, String> headers = mock(MultivaluedMap.class);
+ when(response.getHeaders()).thenReturn(headers);
+ when(headers.get(HttpHeaders.WWW_AUTHENTICATE)).thenReturn(Collections.emptyList());
+
+ assertFalse(authenticator.filterResponseAndAuthenticate(request, response));
+ }
+
+ @Test
+ void filterResponseAndAuthenticateNullListTest() {
+ final BasicAuthenticator authenticator
+ = new BasicAuthenticator(new HttpAuthenticationFilter.Credentials("foo", "bar"));
+ final ClientRequestContext request = mock(ClientRequestContext.class);
+ final ClientResponseContext response = mock(ClientResponseContext.class);
+
+ final MultivaluedMap<String, String> headers = mock(MultivaluedMap.class);
+ when(response.getHeaders()).thenReturn(headers);
+ when(headers.get(HttpHeaders.WWW_AUTHENTICATE)).thenReturn(null);
+
+ assertFalse(authenticator.filterResponseAndAuthenticate(request, response));
+ }
+
+ @Test
+ void filterResponseAndAuthenticateMissingCredentialsMultipleAuthRealmsTest() {
+ final String[] authHeaders = new String[] {
+ "Digest realm=\"test\"",
+ "Basic realm=\"test\""
+ };
+ final BasicAuthenticator authenticator = new BasicAuthenticator(null);
+ final ClientRequestContext request = mock(ClientRequestContext.class);
+ final ClientResponseContext response = mock(ClientResponseContext.class);
+
+ final MultivaluedMap<String, String> headers = mock(MultivaluedMap.class);
+ when(response.getHeaders()).thenReturn(headers);
+ when(headers.get(HttpHeaders.WWW_AUTHENTICATE)).thenReturn(Arrays.asList(authHeaders));
+ when(response.hasEntity()).thenReturn(false);
+
+ assertThrows(ResponseAuthenticationException.class,
+ () -> authenticator.filterResponseAndAuthenticate(request, response));
+ }
+
+}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/innate/http/SSLParamConfiguratorTest.java b/core-client/src/test/java/org/glassfish/jersey/client/innate/http/SSLParamConfiguratorTest.java
index 06dcdec..e7a61d0 100644
--- a/core-client/src/test/java/org/glassfish/jersey/client/innate/http/SSLParamConfiguratorTest.java
+++ b/core-client/src/test/java/org/glassfish/jersey/client/innate/http/SSLParamConfiguratorTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2024, 2025 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
@@ -27,9 +27,13 @@
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.Response;
+import java.net.ConnectException;
import java.net.URI;
import java.util.Collections;
import java.util.List;
@@ -155,4 +159,56 @@
MatcherAssert.assertThat(configurator.isSNIRequired(), Matchers.is(true));
MatcherAssert.assertThat(configurator.getSNIHostName(), Matchers.is("yyy.com"));
}
+
+ @Test
+ public void testIPv6Header() {
+ final String HOST_HEADER_IPv6 = "[172:30::333b]";
+ final URI uri = URI.create("http://[172:30::333a]:8080/api/demo/v1");
+ final JerseyClient client = (JerseyClient) ClientBuilder.newClient();
+ Map<String, List<Object>> httpHeaders = new MultivaluedHashMap<>();
+ httpHeaders.put(HttpHeaders.HOST, Collections.singletonList(HOST_HEADER_IPv6 + ":8080"));
+ SSLParamConfigurator configurator = SSLParamConfigurator.builder()
+ .uri(uri)
+ .headers(httpHeaders)
+ .configuration(client.getConfiguration())
+ .build();
+ MatcherAssert.assertThat(configurator.isSNIRequired(), Matchers.is(true));
+ MatcherAssert.assertThat(configurator.getSNIHostName(), Matchers.is(HOST_HEADER_IPv6));
+ URI expected = URI.create("http://" + HOST_HEADER_IPv6 + ":8080/api/demo/v1");
+ MatcherAssert.assertThat(configurator.getSNIUri(), Matchers.is(expected));
+ MatcherAssert.assertThat(configurator.toIPRequestUri().toString(),
+ Matchers.is(uri.toString().replace("::", ":0:0:0:0:0:")));
+ }
+
+ @Test
+ public void testIpv6Request() {
+ Client client = ClientBuilder.newClient();
+ String u = "http://[::1]:8080";
+ try {
+ client.target(u)
+ .request()
+ .header(HttpHeaders.HOST, "[172:30::333b]:8080")
+ .get();
+ } catch (ProcessingException pe) {
+ if (!ConnectException.class.isInstance(pe.getCause())) {
+ throw pe;
+ }
+ }
+ }
+
+ @Test
+ public void testIpv6RequestNoPort() {
+ Client client = ClientBuilder.newClient();
+ String u = "http://[::1]";
+ try {
+ client.target(u)
+ .request()
+ .header(HttpHeaders.HOST, "[172:30::333b]")
+ .get();
+ } catch (ProcessingException pe) {
+ if (!ConnectException.class.isInstance(pe.getCause())) {
+ throw pe;
+ }
+ }
+ }
}
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
index efa8899..953c27f 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
@@ -134,7 +134,7 @@
*/
@Deprecated
public OutboundMessageContext() {
- this ((Configuration) null);
+ this((Configuration) null);
}
/**
diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassReader.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassReader.java
index f5d846a..e027bdc 100644
--- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassReader.java
+++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassReader.java
@@ -195,7 +195,7 @@
this.b = classFileBuffer;
// Check the class' major_version. This field is after the magic and minor_version fields, which
// use 4 and 2 bytes respectively.
- if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V24) {
+ if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V25) {
throw new IllegalArgumentException(
"Unsupported class file major version " + readShort(classFileOffset + 6));
}
diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/MethodVisitor.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/MethodVisitor.java
index e2f2c92..29eac8f 100644
--- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/MethodVisitor.java
+++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/MethodVisitor.java
@@ -596,7 +596,7 @@
* Visits a LOOKUPSWITCH instruction.
*
* @param dflt beginning of the default handler block.
- * @param keys the values of the keys.
+ * @param keys the values of the keys. Keys must be sorted in increasing order.
* @param labels beginnings of the handler blocks. {@code labels[i]} is the beginning of the
* handler block for the {@code keys[i]} key.
*/
diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Opcodes.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Opcodes.java
index eeb3df7..8a6ca40 100644
--- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Opcodes.java
+++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Opcodes.java
@@ -290,6 +290,7 @@
int V22 = 0 << 16 | 66;
int V23 = 0 << 16 | 67;
int V24 = 0 << 16 | 68;
+ int V25 = 0 << 16 | 69;
/**
* Version flag indicating that the class is using 'preview' features.
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/AnnotationAcceptingListener.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/AnnotationAcceptingListener.java
index 917d094..807252c 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/AnnotationAcceptingListener.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/AnnotationAcceptingListener.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 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
@@ -309,7 +309,7 @@
private static class ClassReaderWrapper {
private static final Logger LOGGER = Logger.getLogger(ClassReader.class.getName());
- private static final int WARN_VERSION = Opcodes.V24;
+ private static final int WARN_VERSION = Opcodes.V25;
private static final int INPUT_STREAM_DATA_CHUNK_SIZE = 4096;
private final byte[] b;
diff --git a/core-server/src/main/resources/META-INF/NOTICE.markdown b/core-server/src/main/resources/META-INF/NOTICE.markdown
index 2016cb4..ab6fea0 100644
--- a/core-server/src/main/resources/META-INF/NOTICE.markdown
+++ b/core-server/src/main/resources/META-INF/NOTICE.markdown
@@ -36,7 +36,7 @@
* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.
* Copyright 2010-2013 Coda Hale and Yammer, Inc.
-org.objectweb.asm Version 9.7.1
+org.objectweb.asm Version 9.8
* License: Modified BSD (https://asm.ow2.io/license.html)
* Copyright: (c) 2000-2011 INRIA, France Telecom. All rights reserved.
diff --git a/examples/NOTICE.md b/examples/NOTICE.md
index b539cdc..70c3d2b 100644
--- a/examples/NOTICE.md
+++ b/examples/NOTICE.md
@@ -96,7 +96,7 @@
* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS
* Copyright: Eric Rowell
-org.objectweb.asm Version 9.7.1
+org.objectweb.asm Version 9.8
* License: Modified BSD (https://asm.ow2.io/license.html)
* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.
diff --git a/examples/expect-100-continue-netty-client/README.MD b/examples/expect-100-continue-netty-client/README.MD
new file mode 100644
index 0000000..5749184
--- /dev/null
+++ b/examples/expect-100-continue-netty-client/README.MD
@@ -0,0 +1,62 @@
+[//]: # " Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. "
+[//]: # " "
+[//]: # " This program and the accompanying materials are made available under the "
+[//]: # " terms of the Eclipse Distribution License v. 1.0, which is available at "
+[//]: # " http://www.eclipse.org/org/documents/edl-v10.php. "
+[//]: # " "
+[//]: # " SPDX-License-Identifier: BSD-3-Clause "
+
+jersey-example-expect-100-continue-netty-connector
+==========================================================
+
+This example demonstrates how to register and run Jersey Netty connector with Expect:100-continue feature on.
+It also provides custom low-level Socket server to demonstrate how is request is captured and processed.
+
+Contents
+--------
+
+The server and client are operating on requests level, without exposing any Resources. Client only sends request in
+form
+```json
+{"message":"Hello from java client"}
+```
+
+Sample Response
+---------------
+Server in turn shows output which demonstrates Expect:100-continue presence and handling
+
+```shell
+==== DUMPING HEADERS ====
+expect, 100-continue
+transfer-encoding, chunked
+host, 127.0.0.1:3000
+content-type, application/json
+accept, application/json
+user-agent, jersey/2.47-snapshot (netty 4.1.112.final)
+==== HEADERS DUMPED =====
+==== DUMPING RESPONSE ====
+HTTP/1.1 100 Continue
+Connection: keep-alive
+
+
+==== RESPONSE DUMPED =====
+24
+{"message":"Hello from java client"}
+==== DUMPING RESPONSE ====
+HTTP/1.1 204 No Content
+Server: Socket Server v.0.0.1
+
+
+```
+
+Running the Example
+-------------------
+
+Run the example using provided ServerSocket container as follows:
+
+> mvn clean compile exec:java
+
+Run the example using client as follows:
+
+> mvn clean package exec:java -Pclient
+
diff --git a/examples/expect-100-continue-netty-client/pom.xml b/examples/expect-100-continue-netty-client/pom.xml
new file mode 100644
index 0000000..e09b0bd
--- /dev/null
+++ b/examples/expect-100-continue-netty-client/pom.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+
+ This program and the accompanying materials are made available under the
+ terms of the Eclipse Distribution License v. 1.0, which is available at
+ http://www.eclipse.org/org/documents/edl-v10.php.
+
+ SPDX-License-Identifier: BSD-3-Clause
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.glassfish.jersey.examples</groupId>
+ <artifactId>project</artifactId>
+ <version>3.0.99-SNAPSHOT</version>
+ </parent>
+
+
+
+ <artifactId>jersey-example-expect-100-continue-netty-client</artifactId>
+ <packaging>jar</packaging>
+ <name>jersey-example-expect-100-continue-netty-client</name>
+
+ <description>Jersey example for Expect: 100-continue header usage with netty connector.</description>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.glassfish.jersey.connectors</groupId>
+ <artifactId>jersey-netty-connector</artifactId>
+ </dependency>
+
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <configuration>
+ <mainClass>${mainClass}</mainClass>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <inherited>true</inherited>
+ <configuration>
+ <showWarnings>false</showWarnings>
+ <fork>false</fork>
+ </configuration>
+ </plugin>
+
+ <!-- Run the application using "mvn jetty:run" -->
+
+ </plugins>
+ </build>
+
+ <profiles>
+ <profile>
+ <id>client</id>
+ <properties>
+ <mainClass>org.glassfish.jersey.examples.expect100continue.netty.connector.NettyClient</mainClass>
+ </properties>
+ </profile>
+ <profile>
+ <id>pre-release</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>xml-maven-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-assembly-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+
+ <properties>
+ <mainClass>org.glassfish.jersey.examples.expect100continue.netty.connector.SocketServer</mainClass>
+ </properties>
+</project>
diff --git a/examples/expect-100-continue-netty-client/src/main/java/org/glassfish/jersey/examples/expect100continue/netty/connector/NettyClient.java b/examples/expect-100-continue-netty-client/src/main/java/org/glassfish/jersey/examples/expect100continue/netty/connector/NettyClient.java
new file mode 100644
index 0000000..fa27674
--- /dev/null
+++ b/examples/expect-100-continue-netty-client/src/main/java/org/glassfish/jersey/examples/expect100continue/netty/connector/NettyClient.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.glassfish.jersey.examples.expect100continue.netty.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.RequestEntityProcessing;
+import org.glassfish.jersey.client.http.Expect100ContinueFeature;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.netty.connector.NettyConnectorProvider;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.Invocation;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class NettyClient {
+ public static void main(String[] args) throws InterruptedException {
+// enableLogging(Level.FINE);
+ test();
+ }
+
+ public static void test() throws InterruptedException {
+ ClientConfig defaultConfig = new ClientConfig();
+ defaultConfig.property(LoggingFeature.LOGGING_FEATURE_VERBOSITY_CLIENT, LoggingFeature.Verbosity.PAYLOAD_ANY);
+
+ //The issue can be produced only by using NettyConnectorProvider
+ defaultConfig.connectorProvider(new NettyConnectorProvider());
+
+ //with below two lines, enabled 100-continue feature
+ defaultConfig.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.CHUNKED);
+ defaultConfig.register(Expect100ContinueFeature.basic());
+
+ Client client = ClientBuilder.newClient(defaultConfig);
+ WebTarget webTarget = client.target("http://127.0.0.1:3000");
+ Invocation.Builder invocationBuilder = webTarget.request();
+ invocationBuilder.header("Accept", "application/json");
+
+ for (int i = 0; i < 5; i++) { //iterating few times here to demonstrate
+ // the 100-continue processing works on any iteration
+
+ System.out.println();
+ System.out.println("****************** Iteration #" + i + " ******************");
+
+ final Response response = invocationBuilder.post(generateSimpleEntity());
+
+ System.out.println("Response status = " + response.getStatus());
+ System.out.println("Response status 204 means No Content, so we do not expect body here");
+ System.out.println("**************************************************");
+ System.out.println();
+ }
+ System.out.println("Client connection should be closed manually with Ctrl-C");
+ }
+
+ private static Entity<String> generateSimpleEntity(){
+ return Entity.entity("{\"message\":\"Hello from java client\"}", MediaType.APPLICATION_JSON_TYPE);
+ }
+
+ private static void enableLogging(Level logLevel) {
+ Logger rootLogger = Logger.getLogger("");
+ rootLogger.setLevel(logLevel);
+ Logger nettyLog = Logger.getLogger("io.netty");
+ nettyLog.setLevel(logLevel);
+ Handler[] handlers = rootLogger.getHandlers();
+ for (final Handler handler : handlers) {
+ handler.setLevel(logLevel);
+ }
+ }
+}
diff --git a/examples/expect-100-continue-netty-client/src/main/java/org/glassfish/jersey/examples/expect100continue/netty/connector/SocketServer.java b/examples/expect-100-continue-netty-client/src/main/java/org/glassfish/jersey/examples/expect100continue/netty/connector/SocketServer.java
new file mode 100644
index 0000000..6fd5699
--- /dev/null
+++ b/examples/expect-100-continue-netty-client/src/main/java/org/glassfish/jersey/examples/expect100continue/netty/connector/SocketServer.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.glassfish.jersey.examples.expect100continue.netty.connector;
+
+import javax.net.ServerSocketFactory;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class SocketServer {
+ private static final String NO_CONTENT_HEADER = "HTTP/1.1 204 No Content";
+ private static final String OK_HEADER = "HTTP/1.1 200 OK";
+ private static final String EXPECT_HEADER = "HTTP/1.1 100 Continue";
+ private final ExecutorService executorService = Executors.newCachedThreadPool();
+ private AtomicBoolean expect_processed = new AtomicBoolean(false);
+
+ private ServerSocket server;
+
+ private static final boolean debug = true;
+
+ private static final int port = 3000;
+
+ private volatile boolean stopped = false;
+
+ public static void main(String args[]) throws IOException {
+ new SocketServer(port).runServer();
+ }
+
+ SocketServer(int port) throws IOException {
+ final ServerSocketFactory socketFactory = ServerSocketFactory.getDefault();
+ server = socketFactory.createServerSocket(port);
+ }
+
+ void stop() {
+ stopped = true;
+ try {
+ server.close();
+ executorService.shutdown();
+ while (!executorService.isTerminated()) {
+ executorService.awaitTermination(100, TimeUnit.MILLISECONDS);
+ }
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ void runServer() {
+
+ executorService.execute(() -> {
+ try {
+ dumpServerReadMe();
+ while (!stopped) {
+ final Socket socket = server.accept();
+ executorService.submit(() -> processRequest(socket));
+ }
+ } catch (IOException e) {
+ if (!stopped) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+
+ private void processRequest(final Socket request) {
+
+ try (final BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
+ final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(request.getOutputStream()))) {
+
+
+ while (!stopped) {
+ final Map<String, String> headers = mapHeaders(reader);
+
+ if (headers.isEmpty()) {
+ continue;
+ }
+ if (debug) {
+ dumpHeaders(headers);
+ }
+
+ boolean failed = processExpect100Continue(headers, writer);
+
+ if (failed) {
+ continue;
+ }
+
+ final String http_header = expect_processed.get() ? NO_CONTENT_HEADER : OK_HEADER;
+ boolean read = readBody(reader, headers);
+
+ final StringBuffer responseBuffer = new StringBuffer(http_header);
+ addNewLineToResponse(responseBuffer);
+ addServerHeaderToResponse(responseBuffer);
+// addToResponse("Content-Length: 0", responseBuffer);
+ addNewLineToResponse(responseBuffer);
+ addNewLineToResponse(responseBuffer);
+ if (debug) {
+ dumpResponse(responseBuffer);
+ }
+
+ writer.write(responseBuffer.toString());
+
+ writer.flush();
+ if (read) {
+ break;
+ }
+
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ request.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void addNewLineToResponse(StringBuffer responseBuffer) {
+ addToResponse("\r\n", responseBuffer);
+ }
+
+ private void addToResponse(String toBeAdded, StringBuffer responseBuffer) {
+ responseBuffer.append(toBeAdded);
+ }
+
+ private void addServerHeaderToResponse(StringBuffer responseBuffer) {
+ addToResponse("Server: Example Socket Server v.0.0.1", responseBuffer);
+ addNewLineToResponse(responseBuffer);
+ }
+
+ private boolean processExpect100Continue(Map<String, String> headers, BufferedWriter writer) throws IOException {
+ String http_header = EXPECT_HEADER;
+ boolean failed = false;
+ final String continueHeader = headers.remove("expect");
+
+ if (continueHeader != null && continueHeader.contains("100-continue")) {
+
+ expect_processed.set(http_header.equals(EXPECT_HEADER));
+
+
+ final StringBuffer responseBuffer = new StringBuffer(http_header);
+
+ addNewLineToResponse(responseBuffer);
+ addToResponse("Connection: keep-alive", responseBuffer);
+ addNewLineToResponse(responseBuffer);
+ addNewLineToResponse(responseBuffer);
+ if (debug) {
+ dumpResponse(responseBuffer);
+ }
+
+ writer.write(responseBuffer.toString());
+ writer.flush();
+ }
+ return failed;
+ }
+
+ private Map<String, String> mapHeaders(BufferedReader reader) throws IOException {
+ String line;
+ final Map<String, String> headers = new HashMap<>();
+
+
+ if (!reader.ready()) {
+ return headers;
+ }
+
+ while ((line = reader.readLine()) != null && !line.isEmpty()) {
+
+
+ int pos = line.indexOf(':');
+ if (pos > -1) {
+ headers.put(
+ line.substring(0, pos).toLowerCase(Locale.ROOT),
+ line.substring(pos + 2).toLowerCase(Locale.ROOT).trim());
+ }
+ }
+
+ return headers;
+ }
+
+ private boolean readBody(BufferedReader reader, Map<String, String> headers) throws IOException {
+ if (headers.containsKey("content-length")) {
+ int contentLength = Integer.valueOf(headers.get("content-length"));
+ int actualLength = 0, readingByte = 0;
+ int[] buffer = new int[contentLength];
+ while (actualLength < contentLength && (readingByte = reader.read()) != -1) {
+ buffer[actualLength++] = readingByte;
+ }
+ if (debug) {
+ System.out.println("Reading " + actualLength + " of " + contentLength + " bytes/chars");
+ }
+ return (actualLength == contentLength);
+ } else if (headers.containsKey("transfer-encoding")) {
+ String line;
+ while ((line = reader.readLine()) != null && !line.equals("0")) {
+ if (debug) {
+ System.out.println(line);
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private void dumpHeaders(Map<String, String> headers) {
+ System.out.println("==== DUMPING HEADERS ====");
+ for (Map.Entry<String, String> entry : headers.entrySet()) {
+ System.out.println(entry.getKey() + ", " + entry.getValue());
+ }
+ System.out.println("==== HEADERS DUMPED =====");
+ }
+
+ private void dumpResponse(StringBuffer responseBuffer) {
+ System.out.println("==== DUMPING RESPONSE ====");
+ System.out.println(responseBuffer);
+ System.out.println("==== RESPONSE DUMPED =====");
+ }
+
+ private void dumpServerReadMe() {
+ System.out.println("==================================Server is running========================================");
+ System.out.println("= *** =");
+ System.out.println("= ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ =");
+ System.out.println("= You can send requests to it either using Netty Client or curl or any other http tool. =");
+ System.out.println("= Try to modify it to see how Expect: 100-continue header works. =");
+ System.out.println("= ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ =");
+ System.out.println("= stop server by Ctrl-c =");
+ System.out.println("= ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ =");
+ System.out.println("= Run server using maven: =");
+ System.out.println("= mvn clean package exec:java =");
+ System.out.println("= Run client using maven: =");
+ System.out.println("= mvn clean package exec:java -Pclient =");
+ System.out.println("= ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ =");
+ System.out.println("= *** =");
+ System.out.println("===========================================================================================");
+ }
+
+}
diff --git a/examples/pom.xml b/examples/pom.xml
index 609875f..3710074 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
- Copyright (c) 2011, 2024 Oracle and/or its affiliates. All rights reserved.
+ Copyright (c) 2011, 2025 Oracle and/or its affiliates. All rights reserved.
This program and the accompanying materials are made available under the
terms of the Eclipse Distribution License v. 1.0, which is available at
@@ -67,6 +67,7 @@
<module>entity-filtering-security</module>
<module>extended-wadl-webapp</module>
<module>exception-mapping</module>
+ <module>expect-100-continue-netty-client</module>
<!--<module>feed-combiner-java8-webapp</module>-->
<module>freemarker-webapp</module>
<!--<module>flight-mgmt-webapp</module>-->
diff --git a/inject/hk2/src/main/java/org/glassfish/jersey/inject/hk2/Hk2RequestScope.java b/inject/hk2/src/main/java/org/glassfish/jersey/inject/hk2/Hk2RequestScope.java
index ef96d16..e9437c4 100644
--- a/inject/hk2/src/main/java/org/glassfish/jersey/inject/hk2/Hk2RequestScope.java
+++ b/inject/hk2/src/main/java/org/glassfish/jersey/inject/hk2/Hk2RequestScope.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2025 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
@@ -16,8 +16,9 @@
package org.glassfish.jersey.inject.hk2;
-import java.util.HashMap;
-import java.util.HashSet;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
@@ -63,7 +64,7 @@
private final AtomicInteger referenceCounter;
private Instance() {
- this.store = new HashMap<>();
+ this.store = new LinkedHashMap<>();
this.referenceCounter = new AtomicInteger(1);
}
@@ -140,7 +141,9 @@
public void release() {
if (referenceCounter.decrementAndGet() < 1) {
try {
- new HashSet<>(store.keySet()).forEach(this::remove);
+ ArrayList<ForeignDescriptor> reverse = new ArrayList<>(store.keySet());
+ Collections.reverse(reverse);
+ reverse.forEach(this::remove);
} finally {
logger.debugLog("Released scope instance {0}", this);
}
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/DefaultJacksonJaxbJsonProvider.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/DefaultJacksonJaxbJsonProvider.java
index 0b9222c..4defc70 100644
--- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/DefaultJacksonJaxbJsonProvider.java
+++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/DefaultJacksonJaxbJsonProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2025 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
@@ -50,6 +50,7 @@
public class DefaultJacksonJaxbJsonProvider extends JacksonJaxbJsonProvider {
private Configuration commonConfig;
private static final Logger LOGGER = Logger.getLogger(DefaultJacksonJaxbJsonProvider.class.getName());
+ private final boolean hasConfig;
@Inject
public DefaultJacksonJaxbJsonProvider(@Context Providers providers, @Context Configuration config) {
@@ -64,10 +65,18 @@
this.commonConfig = config;
_providers = providers;
- Object jaxrsFeatureBag = config.getProperty(JaxrsFeatureBag.JAXRS_FEATURE);
- if (jaxrsFeatureBag != null && (JaxrsFeatureBag.class.isInstance(jaxrsFeatureBag))) {
- ((JaxrsFeatureBag) jaxrsFeatureBag).configureJaxrsFeatures(this);
+ boolean ex = true;
+ try {
+ Object jaxrsFeatureBag = config.getProperty(JaxrsFeatureBag.JAXRS_FEATURE);
+ if (jaxrsFeatureBag != null && (JaxrsFeatureBag.class.isInstance(jaxrsFeatureBag))) {
+ ((JaxrsFeatureBag) jaxrsFeatureBag).configureJaxrsFeatures(this);
+ }
+ } catch (RuntimeException e) {
+ // ignore - not configured
+ LOGGER.fine(LocalizationMessages.ERROR_CONFIGURING(e.getMessage()));
+ ex = false;
}
+ hasConfig = ex;
}
@Override
@@ -82,7 +91,9 @@
@Override
protected JsonEndpointConfig _configForReading(ObjectReader reader, Annotation[] annotations) {
try {
- updateFactoryConstraints(reader.getFactory());
+ if (hasConfig) {
+ updateFactoryConstraints(reader.getFactory());
+ }
} catch (Throwable t) {
// A Jackson 14 would throw NoSuchMethodError, ClassNotFoundException, NoClassDefFoundError or similar
// that should have been ignored
diff --git a/media/json-jackson/src/main/resources/org/glassfish/jersey/jackson/localization.properties b/media/json-jackson/src/main/resources/org/glassfish/jersey/jackson/localization.properties
index 1943306..1c66b9c 100644
--- a/media/json-jackson/src/main/resources/org/glassfish/jersey/jackson/localization.properties
+++ b/media/json-jackson/src/main/resources/org/glassfish/jersey/jackson/localization.properties
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2023, 2024 Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2023, 2025 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
@@ -13,6 +13,7 @@
#
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
#
+error.configuring=Error configuring the DefaultJacksonJaxbJsonProvider: {0}.
error.jackson.streamreadconstraints=Error setting StreamReadConstraints: {0}. Possibly not Jackson 2.15?
error.jackson.streamreadconstraints218=Error setting StreamReadConstraints: {0}. Possibly not Jackson 2.18?
error.modules.not.loaded=Jackson modules could not be loaded: {0}
\ No newline at end of file
diff --git a/media/sse/src/main/java/org/glassfish/jersey/media/sse/internal/EventProcessor.java b/media/sse/src/main/java/org/glassfish/jersey/media/sse/internal/EventProcessor.java
index ee9f56d..8729e4e 100644
--- a/media/sse/src/main/java/org/glassfish/jersey/media/sse/internal/EventProcessor.java
+++ b/media/sse/src/main/java/org/glassfish/jersey/media/sse/internal/EventProcessor.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2025 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
@@ -400,7 +400,7 @@
*/
public Builder reconnectDelay(long reconnectDelay, TimeUnit unit) {
this.reconnectDelay = reconnectDelay;
- this.reconnectUnit = reconnectUnit;
+ this.reconnectUnit = unit;
return this;
}
diff --git a/pom.xml b/pom.xml
index 2ac83ef..eb7f65a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1395,6 +1395,26 @@
</plugins>
</build>
</profile>
+ <profile>
+ <id>scan</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.owasp</groupId>
+ <artifactId>dependency-check-maven</artifactId>
+ <version>12.1.1</version>
+ <!-- mvn -Pscan org.owasp:dependency-check-maven:check -->
+ <executions>
+ <execution>
+ <goals>
+ <goal>check</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
</profiles>
<reporting>
@@ -2132,16 +2152,16 @@
<!-- Versions of Maven plugins -->
<antrun.mvn.plugin.version>3.1.0</antrun.mvn.plugin.version>
- <mvn.ant.version>1.10.14</mvn.ant.version>
+ <mvn.ant.version>1.10.15</mvn.ant.version>
<assembly.mvn.plugin.version>3.7.1</assembly.mvn.plugin.version>
- <clean.mvn.plugin.version>3.4.0</clean.mvn.plugin.version>
+ <clean.mvn.plugin.version>3.4.1</clean.mvn.plugin.version>
<enforcer.mvn.plugin.version>3.5.0</enforcer.mvn.plugin.version>
- <exec.mvn.plugin.version>3.4.1</exec.mvn.plugin.version>
+ <exec.mvn.plugin.version>3.5.0</exec.mvn.plugin.version>
<buildhelper.mvn.plugin.version>3.6.0</buildhelper.mvn.plugin.version>
- <buildnumber.mvn.plugin.version>3.2.0</buildnumber.mvn.plugin.version>
- <checkstyle.mvn.plugin.version>3.4.0</checkstyle.mvn.plugin.version>
- <checkstyle.version>10.17.0</checkstyle.version>
- <compiler.mvn.plugin.version>3.13.0</compiler.mvn.plugin.version>
+ <buildnumber.mvn.plugin.version>3.2.1</buildnumber.mvn.plugin.version>
+ <checkstyle.mvn.plugin.version>3.6.0</checkstyle.mvn.plugin.version>
+ <checkstyle.version>10.23.1</checkstyle.version>
+ <compiler.mvn.plugin.version>3.14.0</compiler.mvn.plugin.version>
<!--
Special version of the compiler plugin just for the jersey-common. All versions above
generate too much for OSGi manifest.mf imports (awt etc). The version 3.11.0 however
@@ -2150,24 +2170,24 @@
-->
<compiler.common.mvn.plugin.version>3.9.0</compiler.common.mvn.plugin.version>
<cyclonedx.mvn.plugin.version>2.8.1</cyclonedx.mvn.plugin.version>
- <dependency.mvn.plugin.version>3.7.1</dependency.mvn.plugin.version>
- <deploy.mvn.plugin.version>3.1.2</deploy.mvn.plugin.version>
+ <dependency.mvn.plugin.version>3.8.1</dependency.mvn.plugin.version>
+ <deploy.mvn.plugin.version>3.1.4</deploy.mvn.plugin.version>
<ear.mvn.plugin.version>3.3.0</ear.mvn.plugin.version>
- <failsafe.mvn.plugin.version>3.3.1</failsafe.mvn.plugin.version>
+ <failsafe.mvn.plugin.version>3.5.3</failsafe.mvn.plugin.version>
<felix.mvn.plugin.version>5.1.9</felix.mvn.plugin.version>
<findbugs.mvn.plugin.version>3.0.5</findbugs.mvn.plugin.version>
<gfembedded.mvn.plugin.version>5.1</gfembedded.mvn.plugin.version>
- <install.mvn.plugin.version>3.1.2</install.mvn.plugin.version>
+ <install.mvn.plugin.version>3.1.4</install.mvn.plugin.version>
<istack.mvn.plugin.version>4.2.0</istack.mvn.plugin.version>
<jar.mvn.plugin.version>3.4.2</jar.mvn.plugin.version>
- <javadoc.mvn.plugin.version>3.8.0</javadoc.mvn.plugin.version>
- <jxr.mvn.plugin.version>3.4.0</jxr.mvn.plugin.version>
+ <javadoc.mvn.plugin.version>3.11.2</javadoc.mvn.plugin.version>
+ <jxr.mvn.plugin.version>3.6.0</jxr.mvn.plugin.version>
<paxexam.mvn.plugin.version>1.2.4</paxexam.mvn.plugin.version>
- <project.info.reports.mvn.plugin.version>3.6.2</project.info.reports.mvn.plugin.version>
+ <project.info.reports.mvn.plugin.version>3.9.0</project.info.reports.mvn.plugin.version>
<resources.mvn.plugin.version>3.3.1</resources.mvn.plugin.version>
<shade.mvn.plugin.version>3.6.0</shade.mvn.plugin.version>
<source.mvn.plugin.version>3.3.1</source.mvn.plugin.version>
- <surefire.mvn.plugin.version>3.5.2</surefire.mvn.plugin.version>
+ <surefire.mvn.plugin.version>3.5.3</surefire.mvn.plugin.version>
<war.mvn.plugin.version>3.4.0</war.mvn.plugin.version>
<wiremock.mvn.plugin.version>2.11.0</wiremock.mvn.plugin.version>
<xml.mvn.plugin.version>1.1.0</xml.mvn.plugin.version>
@@ -2180,34 +2200,34 @@
<arquillian.weld.version>3.0.1.Final</arquillian.weld.version> <!-- 3.0.2.Final fails microprofile TCK tests -->
<!-- asm is now source integrated - keeping this property to see the version -->
<!-- see core-server/src/main/java/jersey/repackaged/asm/.. -->
- <asm.version>9.7.1</asm.version>
+ <asm.version>9.8</asm.version>
<!--required for spring (ext) modules integration -->
<aspectj.weaver.version>1.9.22.1</aspectj.weaver.version>
<!-- <bnd.plugin.version>2.3.6</bnd.plugin.version>-->
<bouncycastle.version>1.70</bouncycastle.version>
- <commons.io.version>2.16.1</commons.io.version>
+ <commons.io.version>2.19.0</commons.io.version>
<commons.codec.version>1.16.1</commons.codec.version>
<!-- <commons-lang3.version>3.3.2</commons-lang3.version>-->
- <commons.logging.version>1.3.4</commons.logging.version>
+ <commons.logging.version>1.3.5</commons.logging.version>
<fasterxml.classmate.version>1.7.0</fasterxml.classmate.version>
<felix.eventadmin.version>1.6.4</felix.eventadmin.version>
<felix.framework.security.version>2.8.4</felix.framework.security.version>
<felix.framework.version>7.0.5</felix.framework.version>
<findbugs.glassfish.version>1.7</findbugs.glassfish.version>
<freemarker.version>2.3.33</freemarker.version>
- <gae.version>2.0.29</gae.version>
- <groovy.version>5.0.0-alpha-11</groovy.version>
- <groovy.jdk8.version>4.0.24</groovy.jdk8.version>
- <gson.version>2.11.0</gson.version>
+ <gae.version>2.0.36</gae.version>
+ <groovy.version>5.0.0-alpha-12</groovy.version>
+ <groovy.jdk8.version>4.0.27</groovy.jdk8.version>
+ <gson.version>2.13.1</gson.version>
<!--versions, extracted here due to maven-enforcer-plugin -->
<!-- <commons.codec.version>1.15</commons.codec.version>-->
<com.uber.jaeger.version>0.27.0</com.uber.jaeger.version>
- <org.codehaus.gmavenplus.version>3.0.2</org.codehaus.gmavenplus.version>
+ <org.codehaus.gmavenplus.version>4.2.0</org.codehaus.gmavenplus.version>
<!-- end of versions extracted here due to maven-enforcer-plugin -->
<!-- micrometer -->
- <micrometer.version>1.12.4</micrometer.version>
+ <micrometer.version>1.15.0</micrometer.version>
<micrometer-tracing.version>1.0.12</micrometer-tracing.version>
<!-- microprofile -->
@@ -2218,7 +2238,7 @@
<helidon.config.11.version>1.4.15</helidon.config.11.version> <!-- JDK 11- support -->
<smallrye.config.version>3.7.1</smallrye.config.version>
- <guava.version>33.3.0-jre</guava.version>
+ <guava.version>33.4.8-jre</guava.version>
<hamcrest.version>3.0</hamcrest.version>
<xmlunit.version>2.10.0</xmlunit.version>
<hk2.osgi.version>org.glassfish.hk2.*;version="[2.5,4)"</hk2.osgi.version>
@@ -2233,17 +2253,17 @@
<jettison.version>1.3.7</jettison.version> <!-- TODO: 1.3.8 doesn't work; AbstractJsonTest complexBeanWithAttributes -->
<jboss.vfs.version>3.3.2.Final</jboss.vfs.version>
<jboss.vfs.jdk8.version>3.2.17.Final</jboss.vfs.jdk8.version>
- <jboss.logging.version>3.6.0.Final</jboss.logging.version>
+ <jboss.logging.version>3.6.1.Final</jboss.logging.version>
<jmh.version>1.37</jmh.version>
<jmockit.version>1.49</jmockit.version>
<junit4.version>4.13.2</junit4.version>
- <junit5.version>5.11.4</junit5.version>
+ <junit5.version>5.12.2</junit5.version>
<junit5.jdk8.version>5.10.3</junit5.jdk8.version>
- <junit-platform-suite.version>1.11.0</junit-platform-suite.version>
+ <junit-platform-suite.version>1.12.2</junit-platform-suite.version>
<kryo.version>4.0.3</kryo.version>
<mockito.version>4.11.0</mockito.version> <!-- CQ 17673 -->
<mustache.version>0.9.14</mustache.version>
- <netty.version>4.1.112.Final</netty.version>
+ <netty.version>4.1.121.Final</netty.version>
<opentracing.version>0.33.0</opentracing.version>
<osgi.version>6.0.0</osgi.version>
<osgi.framework.version>1.10.0</osgi.framework.version>
@@ -2255,9 +2275,9 @@
<rxjava.version>1.3.8</rxjava.version>
<rxjava2.version>2.2.21</rxjava2.version>
<simple.version>6.0.1</simple.version>
- <slf4j.version>2.0.16</slf4j.version>
+ <slf4j.version>2.0.17</slf4j.version>
<spring6.version>6.0.23</spring6.version>
- <testng.version>7.10.2</testng.version>
+ <testng.version>7.11.0</testng.version>
<testng6.version>6.14.3</testng6.version>
<thymeleaf.version>3.1.2.RELEASE</thymeleaf.version>
<!-- Jakartified, eligible for CQ -->
@@ -2306,7 +2326,7 @@
<jetty.osgi.version>org.eclipse.jetty.*;version="[11,15)"</jetty.osgi.version>
<jetty.version>11.0.24</jetty.version>
<jetty.tracing.version>11.0.15</jetty.tracing.version> <!-- special version for tracing support tests, applied before JDK 21-->
- <jetty9.version>9.4.55.v20240627</jetty9.version>
+ <jetty9.version>9.4.57.v20241219</jetty9.version>
<jetty.plugin.version>11.0.24</jetty.plugin.version>
<jetty.servlet.api.25.version>6.1.14</jetty.servlet.api.25.version>
<jsonb.api.version>2.0.0</jsonb.api.version>
diff --git a/tests/e2e-client/pom.xml b/tests/e2e-client/pom.xml
index f96df74..b01f938 100644
--- a/tests/e2e-client/pom.xml
+++ b/tests/e2e-client/pom.xml
@@ -198,6 +198,13 @@
</dependency>
<dependency>
+ <groupId>org.glassfish.jersey.media</groupId>
+ <artifactId>jersey-media-json-jackson</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
<groupId>org.glassfish.jersey.test-framework</groupId>
<artifactId>jersey-test-framework-util</artifactId>
<scope>test</scope>
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RedirectFileUploadServerTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RedirectFileUploadServerTest.java
new file mode 100644
index 0000000..eddcba5
--- /dev/null
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RedirectFileUploadServerTest.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (c) 2025 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.tests.e2e.client;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+
+
+/**
+ * Server for the file upload test that redirects from /submit to /upload.
+ */
+class RedirectFileUploadServerTest {
+ private static final String UPLOAD_DIRECTORY = "target/uploads";
+ private static final String BOUNDARY_PREFIX = "boundary=";
+ private static final Path uploadDir = Paths.get(UPLOAD_DIRECTORY);
+
+ private static HttpServer server;
+
+
+ static void start(int port) throws IOException {
+ // Create upload directory if it doesn't exist
+ if (!Files.exists(uploadDir)) {
+ Files.createDirectory(uploadDir);
+ }
+
+ // Create HTTP server
+ server = HttpServer.create(new InetSocketAddress(port), 0);
+
+ // Create contexts for different endpoints
+ server.createContext("/submit", new SubmitHandler());
+ server.createContext("/upload", new UploadHandler());
+
+ // Set executor and start server
+ server.setExecutor(Executors.newFixedThreadPool(10));
+ server.start();
+ System.out.println("Server running on port " + port);
+ }
+
+ public static void stop() {
+ server.stop(0);
+ }
+
+
+ // Handler for /submit endpoint that redirects to /upload
+ static class SubmitHandler implements HttpHandler {
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ try {
+ if (!"POST".equals(exchange.getRequestMethod())) {
+ sendResponse(exchange, 405, "Method Not Allowed. Only POST is supported.");
+ return;
+ }
+
+ final BufferedReader reader
+ = new BufferedReader(new InputStreamReader(exchange.getRequestBody(), StandardCharsets.UTF_8));
+ while (reader.readLine() != null) {
+ //discard payload - required for JDK 1.8
+ }
+ reader.close();
+
+ // Send a 307 Temporary Redirect to /upload
+ // This preserves the POST method and body in the redirect
+ exchange.getResponseHeaders().add("Location", "/upload");
+ exchange.sendResponseHeaders(307, -1);
+ } finally {
+ exchange.close();
+ }
+ }
+ }
+
+ // Handler for /upload endpoint that processes file uploads
+ static class UploadHandler implements HttpHandler {
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ try {
+ if (!"POST".equals(exchange.getRequestMethod())) {
+ sendResponse(exchange, 405, "Method Not Allowed. Only POST is supported.");
+ return;
+ }
+
+ // Check if the request contains multipart form data
+ String contentType = exchange.getRequestHeaders().getFirst("Content-Type");
+ if (contentType == null || !contentType.startsWith("multipart/form-data")) {
+ sendResponse(exchange, 400, "Bad Request. Content type must be multipart/form-data.");
+ return;
+ }
+
+ // Extract boundary from content type
+ String boundary = extractBoundary(contentType);
+ if (boundary == null) {
+ sendResponse(exchange, 400, "Bad Request. Could not determine boundary.");
+ return;
+ }
+
+ // Process the multipart request and save the file
+ String fileName = processMultipartRequest(exchange, boundary);
+
+ if (fileName != null) {
+ sendResponse(exchange, 200, "File uploaded successfully: " + fileName);
+ } else {
+ sendResponse(exchange, 400, "Bad Request. No file found in request.");
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ sendResponse(exchange, 500, "Internal Server Error: " + e.getMessage());
+ } finally {
+ exchange.close();
+ Files.deleteIfExists(uploadDir);
+ }
+ }
+
+ private String extractBoundary(String contentType) {
+ int boundaryIndex = contentType.indexOf(BOUNDARY_PREFIX);
+ if (boundaryIndex != -1) {
+ return "--" + contentType.substring(boundaryIndex + BOUNDARY_PREFIX.length());
+ }
+ return null;
+ }
+
+ private String processMultipartRequest(HttpExchange exchange, String boundary) throws IOException {
+ InputStream requestBody = exchange.getRequestBody();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(requestBody, StandardCharsets.UTF_8));
+
+ String line;
+ String fileName = null;
+ Path tempFile = null;
+ boolean isFileContent = false;
+
+ // Generate a random filename for the temporary file
+ String tempFileName = UUID.randomUUID().toString();
+ tempFile = Files.createTempFile(tempFileName, ".tmp");
+
+ try (OutputStream fileOut = Files.newOutputStream(tempFile)) {
+ while ((line = reader.readLine()) != null) {
+ // Check for the boundary
+ if (line.startsWith(boundary)) {
+ if (isFileContent) {
+ // We've reached the end of the file content
+ break;
+ }
+
+ // Read the next line (Content-Disposition)
+ line = reader.readLine();
+ if (line != null && line.startsWith("Content-Type")) {
+ line = reader.readLine();
+ }
+ if (line != null && line.contains("filename=")) {
+ // Extract filename
+ int filenameStart = line.indexOf("filename=\"") + 10;
+ int filenameEnd = line.indexOf("\"", filenameStart);
+ fileName = line.substring(filenameStart, filenameEnd);
+
+ // Skip Content-Type line and empty line
+ reader.readLine(); // Content-Type
+// System.out.println(reader.readLine()); // Empty line
+ isFileContent = true;
+ }
+ } else if (isFileContent) {
+ // If we're reading file content and this line is not a boundary,
+ // write it to the file (append a newline unless it's the first line)
+ fileOut.write(line.getBytes(StandardCharsets.UTF_8));
+ fileOut.write('\n');
+ }
+ }
+ }
+
+ // If we found a file, move it from the temp location to the uploads directory
+ if (fileName != null && !fileName.isEmpty()) {
+ Path targetPath = Paths.get(UPLOAD_DIRECTORY, fileName);
+ Files.move(tempFile, targetPath, StandardCopyOption.REPLACE_EXISTING);
+ return fileName;
+ } else {
+ // If no file was found, delete the temp file
+ Files.deleteIfExists(tempFile);
+ return null;
+ }
+ }
+ }
+
+ // Helper method to send HTTP responses
+ private static void sendResponse(HttpExchange exchange, int statusCode, String response) throws IOException {
+ exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8");
+ exchange.sendResponseHeaders(statusCode, response.length());
+ try (OutputStream os = exchange.getResponseBody()) {
+ os.write(response.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RedirectLargeFileTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RedirectLargeFileTest.java
new file mode 100644
index 0000000..52e6cd2
--- /dev/null
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RedirectLargeFileTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2025 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.tests.e2e.client;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.media.multipart.FormDataBodyPart;
+import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
+import org.glassfish.jersey.media.multipart.FormDataMultiPart;
+import org.glassfish.jersey.media.multipart.MultiPartFeature;
+import org.glassfish.jersey.netty.connector.NettyConnectorProvider;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class RedirectLargeFileTest {
+
+ private static final int SERVER_PORT = 9997;
+ private static final String SERVER_ADDR = String.format("http://localhost:%d/submit", SERVER_PORT);
+
+ Client client() {
+ final ClientConfig config = new ClientConfig();
+ config.connectorProvider(new NettyConnectorProvider());
+ config.register(MultiPartFeature.class);
+ return ClientBuilder.newClient(config);
+ }
+
+ @BeforeAll
+ static void startServer() throws Exception{
+ RedirectFileUploadServerTest.start(SERVER_PORT);
+ }
+
+ @AfterAll
+ static void stopServer() {
+ RedirectFileUploadServerTest.stop();
+ }
+
+ @Test
+ void sendFileTest() throws Exception {
+
+ final String fileName = "bigFile.json";
+ final String path = "target/" + fileName;
+
+ final Path pathResource = Paths.get(path);
+ try {
+ final Path realFilePath = Files.createFile(pathResource.toAbsolutePath());
+
+ generateJson(realFilePath.toString(), 1000000); // 33Mb real file size
+
+ final byte[] content = Files.readAllBytes(realFilePath);
+
+ final FormDataMultiPart mp = new FormDataMultiPart();
+ mp.bodyPart(new FormDataBodyPart(FormDataContentDisposition.name(fileName).fileName(fileName).build(),
+ content,
+ MediaType.TEXT_PLAIN_TYPE));
+
+ try (final Response response = client().target(SERVER_ADDR).request()
+ .post(Entity.entity(mp, MediaType.MULTIPART_FORM_DATA_TYPE))) {
+ Assertions.assertEquals(200, response.getStatus());
+ }
+ } finally {
+ Files.deleteIfExists(pathResource);
+ }
+ }
+
+ private static void generateJson(final String filePath, int recordCount) throws Exception {
+
+ try (final JsonGenerator generator = new JsonFactory().createGenerator(new FileWriter(filePath))) {
+ generator.writeStartArray();
+
+ for (int i = 0; i < recordCount; i++) {
+ generator.writeStartObject();
+ generator.writeNumberField("id", i);
+ generator.writeStringField("name", "User" + i);
+ // Add more fields as needed
+ generator.writeEndObject();
+
+ if (i % 10000 == 0) {
+ generator.flush();
+ }
+ }
+
+ generator.writeEndArray();
+ }
+ }
+}
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java
index db08f92..083fda8 100644
--- a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2025 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
@@ -16,34 +16,48 @@
package org.glassfish.jersey.tests.e2e.client.nettyconnector;
-import org.eclipse.jetty.server.Request;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.handler.AbstractHandler;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.RequestEntityProcessing;
import org.glassfish.jersey.client.http.Expect100ContinueFeature;
import org.glassfish.jersey.netty.connector.NettyClientProperties;
import org.glassfish.jersey.netty.connector.NettyConnectorProvider;
-import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
+import javax.net.ServerSocketFactory;
import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.AsyncInvoker;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.Invocation;
+import jakarta.ws.rs.client.InvocationCallback;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
public class Expect100ContinueTest /*extends JerseyTest*/ {
@@ -58,38 +72,32 @@
private static final String RESOURCE_PATH_METHOD_NOT_SUPPORTED = "fail405";
private static final String ENTITY_STRING = "1234567890123456789012345678901234567890123456789012"
- + "3456789012345678901234567890";
+ + "3456789012345678901234567890";
private static final Integer portNumber = 9997;
- private static Server server;
- @BeforeAll
- public static void startExpect100ContinueTestServer() {
- server = new Server(portNumber);
- server.setHandler(new Expect100ContinueTestHandler());
- try {
- server.start();
- } catch (Exception e) {
-
- }
- }
-
- @AfterAll
- public static void stopExpect100ContinueTestServer() {
- try {
- server.stop();
- } catch (Exception e) {
- }
- }
+ private static TestSocketServer server;
private static Client client;
- @BeforeEach
- public void beforeEach() {
+
+ @BeforeAll
+ static void beforeAll() {
final ClientConfig config = new ClientConfig();
- this.configureClient(config);
+ config.connectorProvider(new NettyConnectorProvider());
client = ClientBuilder.newClient(config);
}
+ @BeforeEach
+ void beforeEach() throws IOException {
+ server = new TestSocketServer(portNumber);
+ server.runServer();
+ }
+
+ @AfterEach
+ void afterEach() {
+ server.stop();
+ }
+
private Client client() {
return client;
}
@@ -104,137 +112,353 @@
@Test
public void testExpect100Continue() {
- final Response response = target(RESOURCE_PATH).request().post(Entity.text(ENTITY_STRING));
- assertEquals(200, response.getStatus(), "Expected 200"); //no Expect header sent - response OK
+ final Response response = target(RESOURCE_PATH).request().post(Entity.text(ENTITY_STRING));
+ assertEquals(200, response.getStatus(), "Expected 200"); //no Expect header sent - response OK
}
@Test
public void testExpect100ContinueChunked() {
- final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.basic())
- .property(ClientProperties.REQUEST_ENTITY_PROCESSING,
- RequestEntityProcessing.CHUNKED).request().post(Entity.text(ENTITY_STRING));
- assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
+ final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.basic())
+ .property(ClientProperties.REQUEST_ENTITY_PROCESSING,
+ RequestEntityProcessing.CHUNKED)
+ .request().post(Entity.text(ENTITY_STRING));
+ assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
+ }
+
+ @Test
+ public void testExpect100ContinueManyAsyncRequests() {
+
+ final Invocation.Builder requestBuilder = target(RESOURCE_PATH).register(Expect100ContinueFeature.basic())
+ .property(ClientProperties.REQUEST_ENTITY_PROCESSING,
+ RequestEntityProcessing.CHUNKED)
+ .request();
+ final AsyncInvoker invoker =
+ requestBuilder.async();
+
+ final InvocationCallback<Response> responseCallback = new InvocationCallback<Response>() {
+ @Override
+ public void completed(Response response) {
+ assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
+ }
+
+ @Override
+ public void failed(Throwable throwable) {
+ fail(throwable); // should not fail
+ }
+ };
+ invoker.post(Entity.text(ENTITY_STRING), responseCallback);
+ invoker.post(Entity.text(ENTITY_STRING), responseCallback);
+ invoker.post(Entity.text(ENTITY_STRING), responseCallback);
+ invoker.post(Entity.text(ENTITY_STRING), responseCallback);
+ invoker.post(Entity.text(ENTITY_STRING), responseCallback);
+ invoker.post(Entity.text(ENTITY_STRING), responseCallback);
+
+ final Response response = requestBuilder.post(Entity.text(ENTITY_STRING));
+ assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
}
@Test
public void testExpect100ContinueBuffered() {
- final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.basic())
- .property(ClientProperties.REQUEST_ENTITY_PROCESSING,
- RequestEntityProcessing.BUFFERED).request().header(HttpHeaders.CONTENT_LENGTH, 67000L)
- .post(Entity.text(ENTITY_STRING));
- assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
+ final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.basic())
+ .property(ClientProperties.REQUEST_ENTITY_PROCESSING,
+ RequestEntityProcessing.BUFFERED).request().header(HttpHeaders.CONTENT_LENGTH, 67000L)
+ .post(Entity.text(generateStringByContentLength(67000)));
+ assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
}
@Test
public void testExpect100ContinueCustomLength() {
- final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.withCustomThreshold(100L))
- .request().header(HttpHeaders.CONTENT_LENGTH, Integer.MAX_VALUE)
- .post(Entity.text(ENTITY_STRING));
- assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
+ final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.withCustomThreshold(100L))
+ .request().header(HttpHeaders.CONTENT_LENGTH, 200)
+ .post(Entity.text(generateStringByContentLength(200)));
+ assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
}
@Test
public void testExpect100ContinueCustomLengthWrong() {
- final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.withCustomThreshold(100L))
- .request().header(HttpHeaders.CONTENT_LENGTH, 99L)
- .post(Entity.text(ENTITY_STRING));
- assertEquals(200, response.getStatus(), "Expected 200"); //Expect header NOT sent - low request size
+ final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.withCustomThreshold(100L))
+ .request().header(HttpHeaders.CONTENT_LENGTH, 99L)
+ .post(Entity.text(generateStringByContentLength(99)));
+ assertEquals(200, response.getStatus(), "Expected 200"); //Expect header NOT sent - low request size
}
@Test
public void testExpect100ContinueCustomLengthProperty() {
- final Response response = target(RESOURCE_PATH)
- .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 555L)
- .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
- .register(Expect100ContinueFeature.withCustomThreshold(555L))
- .request().header(HttpHeaders.CONTENT_LENGTH, 666L)
- .post(Entity.text(ENTITY_STRING));
- assertNotNull(response.getStatus()); //Expect header sent - No Content response
+ final Response response = target(RESOURCE_PATH)
+ .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 555L)
+ .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+ .register(Expect100ContinueFeature.withCustomThreshold(555L))
+ .request().header(HttpHeaders.CONTENT_LENGTH, 666L)
+ .post(Entity.text(generateStringByContentLength(666)));
+ assertNotNull(response.getStatus()); //Expect header sent - No Content response
}
@Test
public void testExpect100ContinueRegisterViaCustomProperty() {
- final Response response = target(RESOURCE_PATH)
- .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
- .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
- .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
- .post(Entity.text(ENTITY_STRING));
- assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
+ final Response response = target(RESOURCE_PATH)
+ .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
+ .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+ .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
+ .post(Entity.text(generateStringByContentLength(44)));
+ assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
}
@Test
public void testExpect100ContinueNotSupported() {
- final Response response = target(RESOURCE_PATH_NOT_SUPPORTED)
- .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
- .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
- .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
- .post(Entity.text(ENTITY_STRING));
- assertEquals(417, response.getStatus(), "Expected 417"); //Expectations not supported
+ final Response response = target(RESOURCE_PATH_NOT_SUPPORTED)
+ .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
+ .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+ .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
+ .post(Entity.text(generateStringByContentLength(44)));
+ assertEquals(204, response.getStatus(),
+ "This should re-send request without expect and obtain the 204 response code"); //Expectations not supported
}
@Test
public void testExpect100ContinueUnauthorized() {
- assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_UNAUTHORIZED)
- .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
- .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
- .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000)
- .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
- .post(Entity.text(ENTITY_STRING)));
+ assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_UNAUTHORIZED)
+ .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
+ .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+ .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000)
+ .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
+ .post(Entity.text(generateStringByContentLength(44))));
}
@Test
public void testExpect100ContinuePayloadTooLarge() {
assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_PAYLOAD_TOO_LARGE)
- .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
- .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
- .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000)
- .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
- .post(Entity.text(ENTITY_STRING)));
+ .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
+ .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+ .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000)
+ .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
+ .post(Entity.text(generateStringByContentLength(44))));
}
@Test
public void testExpect100ContinueMethodNotSupported() {
- assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_METHOD_NOT_SUPPORTED)
- .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
- .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
- .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000)
- .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
- .post(Entity.text(ENTITY_STRING)));
+ assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_METHOD_NOT_SUPPORTED)
+ .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
+ .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+ .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000)
+ .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
+ .post(Entity.text(generateStringByContentLength(44))));
+
}
- static class Expect100ContinueTestHandler extends AbstractHandler {
- @Override
- public void handle(String target,
- Request baseRequest,
- HttpServletRequest request,
- HttpServletResponse response) throws IOException {
- boolean expected = request.getHeader("Expect") != null;
- boolean failed = false;
- if (target.equals("/" + RESOURCE_PATH_NOT_SUPPORTED)) {
- response.sendError(417);
- failed = true;
- }
- if (target.equals("/" + RESOURCE_PATH_UNAUTHORIZED)) {
- response.sendError(401);
- failed = true;
- }
- if (target.equals("/" + RESOURCE_PATH_PAYLOAD_TOO_LARGE)) {
- response.sendError(413);
- failed = true;
- }
- if (target.equals("/" + RESOURCE_PATH_METHOD_NOT_SUPPORTED)) {
- response.sendError(405);
- failed = true;
- }
- if (expected && !failed) {
- System.out.println("Expect:100-continue found, sending response header");
- response.setStatus(204);
- }
- response.getWriter().println();
- response.flushBuffer();
- baseRequest.setHandled(true);
-
- request.getReader().lines().forEach(System.out::println);
+ private String generateStringByContentLength(int length) {
+ final char[] array = new char[length];
+ final Random r = new Random();
+ for (int i = 0; i < length; i++) {
+ array[i] = ENTITY_STRING.charAt(r.nextInt(ENTITY_STRING.length()));
}
+ return String.valueOf(array);
+ }
+
+ private static final class TestSocketServer {
+
+ private static final String NO_CONTENT_HEADER = "HTTP/1.1 204 No Content";
+ private static final String OK_HEADER = "HTTP/1.1 200 OK";
+ private static final String EXPECT_HEADER = "HTTP/1.1 100 Continue";
+ private static final String UNAUTHORIZED_HEADER = "HTTP/1.1 401 Unauthorized";
+ private static final String NOT_SUPPORTED_HEADER = "HTTP/1.1 405 Method Not Allowed";
+ private static final String TOO_LARGE_HEADER = "HTTP/1.1 413 Request Entity Too Large";
+
+ private final ExecutorService executorService = Executors.newCachedThreadPool();
+ private AtomicBoolean unauthorized = new AtomicBoolean(false);
+ private AtomicBoolean not_supported = new AtomicBoolean(false);
+ private AtomicBoolean too_large = new AtomicBoolean(false);
+
+ private AtomicBoolean expect_processed = new AtomicBoolean(false);
+
+ private ServerSocket server;
+
+ private volatile boolean stopped = false;
+
+ public TestSocketServer(int port) throws IOException {
+ final ServerSocketFactory socketFactory = ServerSocketFactory.getDefault();
+ server = socketFactory.createServerSocket(port);
+ }
+
+ void stop() {
+ stopped = true;
+ try {
+ server.close();
+ executorService.shutdown();
+ while (!executorService.isTerminated()) {
+ executorService.awaitTermination(100, TimeUnit.MILLISECONDS);
+ }
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ void runServer() {
+
+ executorService.execute(() -> {
+ try {
+ while (!stopped) {
+ final Socket socket = server.accept();
+ executorService.submit(() -> processRequest(socket));
+ }
+ } catch (IOException e) {
+ if (!stopped) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+
+ private void processRequest(final Socket request) {
+ try (final BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
+ final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(request.getOutputStream()))) {
+
+
+ while (!stopped) {
+ final Map<String, String> headers = mapHeaders(reader);
+
+ if (headers.isEmpty()) {
+ continue;
+ }
+
+ boolean failed = processExpect100Continue(headers, writer);
+
+ if (failed) {
+ continue;
+ }
+
+ final String http_header = expect_processed.get() ? NO_CONTENT_HEADER : OK_HEADER;
+ boolean read = readBody(reader, headers);
+
+ final StringBuffer responseBuffer = new StringBuffer(http_header);
+ addNewLineToResponse(responseBuffer);
+ addServerHeaderToResponse(responseBuffer);
+ addNewLineToResponse(responseBuffer);
+ addNewLineToResponse(responseBuffer);
+
+ writer.write(responseBuffer.toString());
+
+ writer.flush();
+ if (read) {
+ break;
+ }
+
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ request.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void addNewLineToResponse(StringBuffer responseBuffer) {
+ addToResponse("\r\n", responseBuffer);
+ }
+
+ private void addToResponse(String toBeAdded, StringBuffer responseBuffer) {
+ responseBuffer.append(toBeAdded);
+ }
+
+ private void addServerHeaderToResponse(StringBuffer responseBuffer) {
+ addToResponse("Server: SocketServer v.0.0.1", responseBuffer);
+ addNewLineToResponse(responseBuffer);
+ }
+
+ private boolean processExpect100Continue(Map<String, String> headers, BufferedWriter writer) throws IOException {
+ String http_header = EXPECT_HEADER;
+ boolean failed = false;
+ final String continueHeader = headers.remove("expect");
+
+ if (continueHeader != null && continueHeader.contains("100-continue")) {
+
+ if (unauthorized.get()) {
+ http_header = UNAUTHORIZED_HEADER;
+ unauthorized.set(false);
+ failed = true;
+ }
+
+ if (not_supported.get()) {
+ http_header = NOT_SUPPORTED_HEADER;
+ not_supported.set(false);
+ failed = true;
+ }
+
+ if (too_large.get()) {
+ http_header = TOO_LARGE_HEADER;
+ too_large.set(false);
+ failed = true;
+ }
+
+ expect_processed.set(http_header.equals(EXPECT_HEADER));
+
+
+ final StringBuffer responseBuffer = new StringBuffer(http_header);
+
+ addNewLineToResponse(responseBuffer);
+ addToResponse("Connection: keep-alive", responseBuffer);
+ addNewLineToResponse(responseBuffer);
+ addNewLineToResponse(responseBuffer);
+
+ writer.write(responseBuffer.toString());
+ writer.flush();
+ }
+ return failed;
+ }
+
+ private Map<String, String> mapHeaders(BufferedReader reader) throws IOException {
+ String line;
+ final Map<String, String> headers = new HashMap<>();
+
+
+ if (!reader.ready()) {
+ return headers;
+ }
+
+ while ((line = reader.readLine()) != null && !line.isEmpty()) {
+
+ if (line.contains(RESOURCE_PATH_UNAUTHORIZED)) {
+ unauthorized.set(true);
+ }
+
+ if (line.contains(RESOURCE_PATH_METHOD_NOT_SUPPORTED)) {
+ not_supported.set(true);
+ }
+
+ if (line.contains(RESOURCE_PATH_PAYLOAD_TOO_LARGE)) {
+ too_large.set(true);
+ }
+
+ int pos = line.indexOf(':');
+ if (pos > -1) {
+ headers.put(
+ line.substring(0, pos).toLowerCase(Locale.ROOT),
+ line.substring(pos + 2).toLowerCase(Locale.ROOT).trim());
+ }
+ }
+
+ return headers;
+ }
+
+ private boolean readBody(BufferedReader reader, Map<String, String> headers) throws IOException, InterruptedException {
+ if (headers.containsKey("content-length")) {
+ int contentLength = Integer.valueOf(headers.get("content-length"));
+ int actualLength = 0, readingByte = 0;
+ int[] buffer = new int[contentLength];
+ while (actualLength < contentLength && (readingByte = reader.read()) != -1) {
+ buffer[actualLength++] = readingByte;
+ }
+ return (actualLength == contentLength);
+ } else if (headers.containsKey("transfer-encoding")) {
+ String line;
+ while ((line = reader.readLine()) != null && !line.equals("0")) {
+ }
+ return true;
+ }
+ return false;
+ }
+
}
}
\ No newline at end of file
diff --git a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/process/internal/RequestScopeTest.java b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/process/internal/RequestScopeTest.java
index 7582894..d0488fa 100644
--- a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/process/internal/RequestScopeTest.java
+++ b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/process/internal/RequestScopeTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 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
@@ -17,6 +17,8 @@
package org.glassfish.jersey.tests.e2e.common.process.internal;
import java.lang.reflect.Type;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
import org.glassfish.jersey.inject.hk2.Hk2RequestScope;
import org.glassfish.jersey.internal.inject.ForeignDescriptor;
@@ -25,6 +27,7 @@
import org.glassfish.hk2.api.ServiceHandle;
import org.glassfish.hk2.utilities.AbstractActiveDescriptor;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -140,6 +143,34 @@
assertNull(instance.get(inhab));
}
+ @Test
+ public void testOrderOfRelease() {
+ final RequestScope requestScope = new Hk2RequestScope();
+ final AtomicInteger instanceRelease = new AtomicInteger(0);
+ final Hk2RequestScope.Instance instance = requestScope.runInScope(() -> {
+ final Hk2RequestScope.Instance internalInstance = (Hk2RequestScope.Instance) requestScope.current();
+ for (int index = 1; index != 10; index++) {
+ final int in = index;
+ TestProvider testProvider = new TestProvider(String.valueOf(in)) {
+ @Override
+ public int hashCode() {
+ return super.hashCode() + in;
+ }
+ };
+ final ForeignDescriptor fd = ForeignDescriptor.wrap(testProvider, new Consumer<Object>() {
+ @Override
+ public void accept(Object o) {
+ instanceRelease.set(instanceRelease.get() * 10 + in);
+ }
+ });
+ internalInstance.put(fd, String.valueOf(index));
+ }
+ return internalInstance;
+ });
+ instance.release();
+ Assertions.assertEquals(987654321, instanceRelease.get());
+ }
+
/**
* Test request scope inhabitant.
*/
diff --git a/tests/e2e-server/pom.xml b/tests/e2e-server/pom.xml
index 215cbfc..e0d7298 100644
--- a/tests/e2e-server/pom.xml
+++ b/tests/e2e-server/pom.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
- Copyright (c) 2017, 2024 Oracle and/or its affiliates. All rights reserved.
+ Copyright (c) 2017, 2025 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
@@ -210,6 +210,19 @@
</dependency>
<dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-servlet</artifactId>
+ <version>${jetty.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>${commons.io.version}</version>
+ </dependency>
+
+ <dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<scope>test</scope>
@@ -224,6 +237,34 @@
<profiles>
<profile>
+ <id>JettyTestExclude</id>
+ <activation>
+ <jdk>1.8</jdk>
+ </activation>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>default-testCompile</id>
+ <phase>test-compile</phase>
+ <configuration>
+ <testExcludes>
+ <testExclude>org/glassfish/jersey/tests/e2e/server/SimilarInputStreamTest.java</testExclude>
+ </testExcludes>
+ </configuration>
+ <goals>
+ <goal>testCompile</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
<id>xdk</id>
<properties>
<!-- do not use security manager for xdk -->
diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/SimilarInputStreamTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/SimilarInputStreamTest.java
new file mode 100644
index 0000000..cedad3a
--- /dev/null
+++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/SimilarInputStreamTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (c) 2025 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.tests.e2e.server;
+
+import org.apache.commons.io.IOUtils;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.internal.InternalProperties;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.glassfish.jersey.jetty.JettyHttpContainerFactory;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.spi.TestContainer;
+import org.glassfish.jersey.test.spi.TestContainerException;
+import org.glassfish.jersey.test.spi.TestContainerFactory;
+import org.junit.jupiter.api.Test;
+
+import jakarta.servlet.DispatcherType;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ReadListener;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.Invocation;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.util.Collections;
+import java.util.EnumSet;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class SimilarInputStreamTest extends JerseyTest {
+
+ @Override
+ protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
+ return (baseUri, deploymentContext) -> {
+ final Server server = JettyHttpContainerFactory.createServer(baseUri, false);
+ final ServerConnector connector = new ServerConnector(server);
+ connector.setPort(9001);
+ server.addConnector(connector);
+
+ final ServletContainer jerseyServletContainer = new ServletContainer(deploymentContext.getResourceConfig());
+ final ServletHolder jettyServletHolder = new ServletHolder(jerseyServletContainer);
+
+ final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
+ context.setContextPath("/");
+
+ // filter which will change the http servlet request to have a reply-able input stream
+ context.addFilter(FilterSettingMultiReadRequest.class,
+ "/*", EnumSet.allOf(DispatcherType.class));
+ context.addServlet(jettyServletHolder, "/api/*");
+
+ server.setHandler(context);
+ return new TestContainer() {
+ @Override
+ public ClientConfig getClientConfig() {
+ return new ClientConfig();
+ }
+
+ @Override
+ public URI getBaseUri() {
+ return baseUri;
+ }
+
+ @Override
+ public void start() {
+ try {
+ server.start();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void stop() {
+ try {
+ server.stop();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ };
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig resourceConfig = new ResourceConfig(TestResource.class);
+ // force jersey to use jackson for deserialization
+ resourceConfig.addProperties(
+ Collections.singletonMap(InternalProperties.JSON_FEATURE, JacksonFeature.class.getSimpleName()));
+ return resourceConfig;
+ }
+
+ @Test
+ public void readJsonWithReplayableInputStreamFailsTest() {
+ final Invocation.Builder requestBuilder = target("/api/v1/echo").request();
+ final MyDto myDto = new MyDto();
+ myDto.setMyField("Something");
+ try (Response response = requestBuilder.post(Entity.entity(myDto, MediaType.APPLICATION_JSON))) {
+ // fixed from failure with a 400 as jackson can never finish reading the input stream
+ assertEquals(200, response.getStatus());
+ final MyDto resultDto = response.readEntity(MyDto.class);
+ assertEquals("Something", resultDto.getMyField()); //verify we still get Something
+ }
+ }
+
+ @Path("/v1")
+ public static class TestResource {
+
+ @POST
+ @Path("/echo")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Consumes(MediaType.APPLICATION_JSON)
+ public MyDto echo(MyDto input) {
+ return input;
+ }
+ }
+
+ public static class MyDto {
+ private String myField;
+
+ public String getMyField() {
+ return myField;
+ }
+
+ public void setMyField(String myField) {
+ this.myField = myField;
+ }
+
+ @Override
+ public String toString() {
+ return "MyDto{"
+ + "myField='" + myField + '\''
+ + '}';
+ }
+ }
+
+
+ public static class FilterSettingMultiReadRequest implements Filter {
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response,
+ FilterChain chain) throws IOException, ServletException {
+ /* wrap the request in order to read the inputstream multiple times */
+ MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) request);
+ chain.doFilter(multiReadRequest, response);
+ }
+ }
+
+ static class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
+ private byte[] cachedBytes;
+
+ public MultiReadHttpServletRequest(HttpServletRequest request) {
+ super(request);
+ }
+
+ @Override
+ public ServletInputStream getInputStream() throws IOException {
+ if (cachedBytes == null) {
+ cacheInputStream();
+ }
+
+ return new CachedServletInputStream(cachedBytes);
+ }
+
+ @Override
+ public BufferedReader getReader() throws IOException {
+ return new BufferedReader(new InputStreamReader(getInputStream()));
+ }
+
+ private void cacheInputStream() throws IOException {
+ // Cache the inputstream in order to read it multiple times.
+ cachedBytes = IOUtils.toByteArray(super.getInputStream());
+ }
+
+
+ /* An input stream which reads the cached request body */
+ private class CachedServletInputStream extends ServletInputStream {
+
+ private final ByteArrayInputStream buffer;
+
+ public CachedServletInputStream(byte[] contents) {
+ this.buffer = new ByteArrayInputStream(contents);
+ }
+
+ @Override
+ public int read() {
+ return buffer.read();
+ }
+
+ @Override
+ public boolean isFinished() {
+ return buffer.available() == 0;
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public void setReadListener(ReadListener listener) {
+ throw new RuntimeException("Not implemented");
+ }
+ }
+ }
+}
diff --git a/tests/integration/jersey-5796/pom.xml b/tests/integration/jersey-5796/pom.xml
index 2710d8e..8ab2985 100644
--- a/tests/integration/jersey-5796/pom.xml
+++ b/tests/integration/jersey-5796/pom.xml
@@ -23,7 +23,7 @@
<parent>
<artifactId>project</artifactId>
<groupId>org.glassfish.jersey.tests.integration</groupId>
- <version>2.47-SNAPSHOT</version>
+ <version>3.0.99-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/tests/integration/jersey-5796/src/test/java/org/glassfish/jersey/tests/integration/jersey5796/Jersey5796Test.java b/tests/integration/jersey-5796/src/test/java/org/glassfish/jersey/tests/integration/jersey5796/Jersey5796Test.java
index 00705b1..80ade6a 100644
--- a/tests/integration/jersey-5796/src/test/java/org/glassfish/jersey/tests/integration/jersey5796/Jersey5796Test.java
+++ b/tests/integration/jersey-5796/src/test/java/org/glassfish/jersey/tests/integration/jersey5796/Jersey5796Test.java
@@ -18,26 +18,19 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.lang.ref.WeakReference;
-import java.lang.reflect.Field;
-import java.util.Map;
-import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicInteger;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.client.Client;
-import javax.ws.rs.client.ClientBuilder;
-import javax.ws.rs.core.Application;
-import javax.ws.rs.core.GenericType;
-import javax.ws.rs.core.Response;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.GenericType;
+import jakarta.ws.rs.core.Response;
import org.glassfish.jersey.client.ChunkedInput;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientLifecycleListener;
-import org.glassfish.jersey.client.JerseyClient;
import org.glassfish.jersey.server.ChunkedOutput;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
diff --git a/tests/integration/pom.xml b/tests/integration/pom.xml
index 4f5245f..180a564 100644
--- a/tests/integration/pom.xml
+++ b/tests/integration/pom.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
- Copyright (c) 2011, 2024 Oracle and/or its affiliates. All rights reserved.
+ Copyright (c) 2011, 2025 Oracle and/or its affiliates. All rights reserved.
Copyright (c) 2018 Payara Foundation and/or its affiliates. All rights reserved.
This program and the accompanying materials are made available under the
@@ -58,6 +58,7 @@
<module>jersey-4542</module>
<module>jersey-4697</module>
<module>jersey-4722</module>
+ <module>jersey-5796</module>
<module>microprofile</module>
<module>reactive-streams</module>
<module>jersey-5087</module>
@@ -164,6 +165,7 @@
<module>jersey-3796</module>
<module>jersey-4949</module>
<module>property-check</module>
+ <module>resteasy-client</module>
<module>security-digest</module>
<module>servlet-2.5-autodiscovery-1</module>
<module>servlet-2.5-autodiscovery-2</module>
diff --git a/tests/integration/resteasy-client/pom.xml b/tests/integration/resteasy-client/pom.xml
new file mode 100644
index 0000000..7c4c056
--- /dev/null
+++ b/tests/integration/resteasy-client/pom.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Copyright (c) 2025 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
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.glassfish.jersey.tests.integration</groupId>
+ <artifactId>project</artifactId>
+ <version>3.0.99-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>resteasy-client</artifactId>
+
+ <packaging>war</packaging>
+ <name>jersey-tests-integration-resteasy-client</name>
+
+ <description>
+ Jersey tests for Spring.Boot / Resteasy integration
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>resteasy-client</artifactId>
+ <version>6.0.3.Final</version> <!-- according to https://docs.resteasy.dev/6.2/userguide/ -->
+<!-- the 6.0.x versions are for Jakarta EE9 (Jakarta RESTful Web Services API 3.0 )-->
+ <exclusions>
+ <exclusion>
+ <artifactId>commons-logging</artifactId>
+ <groupId>commons-logging</groupId>
+ </exclusion>
+ <exclusion>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
+ <groupId>org.glassfish.jersey.test-framework</groupId>
+ <artifactId>jersey-test-framework-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.glassfish.jersey.containers</groupId>
+ <artifactId>jersey-container-servlet</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+ <artifactId>jersey-test-framework-provider-external</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+ <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.core</groupId>
+ <artifactId>jersey-client</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-logging</groupId>
+ <artifactId>commons-logging</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.media</groupId>
+ <artifactId>jersey-media-json-jackson</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/tests/integration/resteasy-client/src/test/java/org/glassfish/jersey/tests/springboot/RestEasyClientTest.java b/tests/integration/resteasy-client/src/test/java/org/glassfish/jersey/tests/springboot/RestEasyClientTest.java
new file mode 100644
index 0000000..4848388
--- /dev/null
+++ b/tests/integration/resteasy-client/src/test/java/org/glassfish/jersey/tests/springboot/RestEasyClientTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2025 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.tests.springboot;
+
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Configuration;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.Providers;
+
+import org.glassfish.jersey.jackson.internal.DefaultJacksonJaxbJsonProvider;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.jboss.resteasy.client.jaxrs.ResteasyClient;
+import org.jboss.resteasy.client.jaxrs.internal.ResteasyClientBuilderImpl;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+public class RestEasyClientTest extends JerseyTest {
+
+ private static final CountDownLatch readFromLatch = new CountDownLatch(1);
+
+ @Path("/")
+ public static class RestEasyClientTestResource {
+ @POST
+ @Path("/test")
+ @Produces(MediaType.APPLICATION_JSON)
+ public String testPost(String echo) {
+ return echo;
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(RestEasyClientTestResource.class);
+ }
+
+ @Test
+ public void test() throws InterruptedException {
+ AtomicReference<String> messageRef = new AtomicReference<>();
+ Logger logger = Logger.getLogger(DefaultJacksonJaxbJsonProvider.class.getName());
+ logger.addHandler(new ConsoleHandler() {
+ @Override
+ public void publish(LogRecord record) {
+ messageRef.set(record.getMessage());
+ }
+ });
+ logger.setLevel(Level.FINE);
+
+ final ResteasyClient client = new ResteasyClientBuilderImpl().build();
+
+ client.register(TestDefaultJacksonJaxbJsonProvider.class);
+
+ try (final Response r = client.target(target().getUri()).path("/test")
+ .request().post(Entity.entity("{\"test\": \"test\"}", MediaType.APPLICATION_JSON))) {
+ Object o = r.readEntity(Object.class);
+ Assertions.assertTrue(o.toString().contains("test"));
+ readFromLatch.await();
+ Assertions.assertEquals(0, readFromLatch.getCount(), "DefaultJacksonJaxbJsonProvider has not been used");
+ }
+
+ client.close();
+ MatcherAssert.assertThat(messageRef.get(), Matchers.notNullValue());
+
+ }
+
+ public static class TestDefaultJacksonJaxbJsonProvider extends DefaultJacksonJaxbJsonProvider {
+ public TestDefaultJacksonJaxbJsonProvider(@Context Providers providers, @Context Configuration config) {
+ super(providers, config);
+ }
+
+ @Override
+ public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType,
+ MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException {
+ readFromLatch.countDown();
+ return super.readFrom(type, genericType, annotations, mediaType, httpHeaders, entityStream);
+ }
+ }
+
+}