diff --git a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java index ba2b27b520b4..498b54fd9dbb 100644 --- a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java +++ b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.http2.server; +import java.io.EOFException; import java.util.Map; import java.util.concurrent.TimeoutException; @@ -155,7 +156,7 @@ public void onDataAvailable(Stream stream) @Override public void onReset(Stream stream, ResetFrame frame, Callback callback) { - EofException failure = new EofException("Reset " + ErrorCode.toString(frame.getError(), null)); + EOFException failure = new EOFException("Reset " + ErrorCode.toString(frame.getError(), null)); onFailure(stream, failure, callback); } diff --git a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HttpStreamOverHTTP2.java b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HttpStreamOverHTTP2.java index 9de030266032..f03e9479843f 100644 --- a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HttpStreamOverHTTP2.java +++ b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HttpStreamOverHTTP2.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.http2.server.internal; +import java.io.EOFException; import java.nio.ByteBuffer; import java.util.concurrent.TimeoutException; import java.util.function.BiConsumer; @@ -38,6 +39,7 @@ import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.EofException; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpStream; import org.eclipse.jetty.server.Request; @@ -587,7 +589,8 @@ public void onTimeout(TimeoutException timeout, BiConsumer co @Override public Runnable onFailure(Throwable failure, Callback callback) { - Runnable runnable = _httpChannel.onFailure(failure); + boolean remote = failure instanceof EOFException; + Runnable runnable = remote ? _httpChannel.onRemoteFailure(new EofException(failure)) : _httpChannel.onFailure(failure); return () -> { if (runnable != null) diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AbstractTest.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AbstractTest.java index 90bc346b4ce9..9a5f944f14da 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AbstractTest.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AbstractTest.java @@ -58,7 +58,12 @@ public class AbstractTest protected void start(Handler handler) throws Exception { - HTTP2CServerConnectionFactory connectionFactory = new HTTP2CServerConnectionFactory(new HttpConfiguration()); + start(handler, new HttpConfiguration()); + } + + protected void start(Handler handler, HttpConfiguration httpConfiguration) throws Exception + { + HTTP2CServerConnectionFactory connectionFactory = new HTTP2CServerConnectionFactory(httpConfiguration); connectionFactory.setInitialSessionRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE); connectionFactory.setInitialStreamRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE); prepareServer(connectionFactory); diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java index 430d721bcf7c..c32a6969fcef 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java @@ -17,22 +17,33 @@ import java.nio.ByteBuffer; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.ErrorCode; import org.eclipse.jetty.http2.api.Session; import org.eclipse.jetty.http2.api.Stream; import org.eclipse.jetty.http2.frames.DataFrame; import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.frames.ResetFrame; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.EofException; import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.FuturePromise; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -241,6 +252,67 @@ public void onClosed(Stream stream) */ } + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testClientResetRemoteErrorNotification(boolean notify) throws Exception + { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference responseRef = new AtomicReference<>(); + AtomicReference failureRef = new AtomicReference<>(); + HttpConfiguration httpConfiguration = new HttpConfiguration(); + httpConfiguration.setNotifyRemoteAsyncErrors(notify); + start(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + request.addFailureListener(failureRef::set); + responseRef.set(response); + latch.countDown(); + return true; + } + }, httpConfiguration); + + Session session = newClientSession(new Session.Listener() {}); + MetaData.Request metaData = newRequest("GET", HttpFields.EMPTY); + HeadersFrame frame = new HeadersFrame(metaData, null, true); + FuturePromise promise = new FuturePromise<>(); + session.newStream(frame, promise, null); + Stream stream = promise.get(5, TimeUnit.SECONDS); + + // Wait for the server to be idle. + assertTrue(latch.await(5, TimeUnit.SECONDS)); + sleep(500); + + stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP); + + if (notify) + // Wait for the reset to be notified to the failure listener. + await().atMost(5, TimeUnit.SECONDS).until(failureRef::get, instanceOf(EofException.class)); + else + // Wait for the reset to NOT be notified to the failure listener. + await().atMost(5, TimeUnit.SECONDS).during(1, TimeUnit.SECONDS).until(failureRef::get, nullValue()); + + // Assert that writing to the response fails. + var cb = new Callback() + { + private Throwable failure = null; + + @Override + public void failed(Throwable x) + { + failure = x; + } + + Throwable failure() + { + return failure; + } + }; + responseRef.get().write(true, BufferUtil.EMPTY_BUFFER, cb); + await().atMost(5, TimeUnit.SECONDS).until(cb::failure, instanceOf(EofException.class)); + } + private static void sleep(long ms) throws InterruptedIOException { try diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncServletTest.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncServletTest.java index 328e21571962..5ca981b723b3 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncServletTest.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncServletTest.java @@ -154,58 +154,6 @@ public void test() // assertTrue(clientLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS)); // } // -// @Test -// public void testStartAsyncThenClientResetWithoutRemoteErrorNotification() throws Exception -// { -// HttpConfiguration httpConfiguration = new HttpConfiguration(); -// httpConfiguration.setNotifyRemoteAsyncErrors(false); -// prepareServer(new HTTP2ServerConnectionFactory(httpConfiguration)); -// ServletContextHandler context = new ServletContextHandler(server, "/"); -// AtomicReference asyncContextRef = new AtomicReference<>(); -// CountDownLatch latch = new CountDownLatch(1); -// context.addServlet(new ServletHolder(new HttpServlet() -// { -// @Override -// protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException -// { -// AsyncContext asyncContext = request.startAsync(); -// asyncContext.setTimeout(0); -// asyncContextRef.set(asyncContext); -// latch.countDown(); -// } -// }), servletPath + "/*"); -// server.start(); -// -// prepareClient(); -// client.start(); -// Session session = newClient(new Session.Listener() {}); -// MetaData.Request metaData = newRequest("GET", HttpFields.EMPTY); -// HeadersFrame frame = new HeadersFrame(metaData, null, true); -// FuturePromise promise = new FuturePromise<>(); -// session.newStream(frame, promise, null); -// Stream stream = promise.get(5, TimeUnit.SECONDS); -// -// // Wait for the server to be in ASYNC_WAIT. -// assertTrue(latch.await(5, TimeUnit.SECONDS)); -// sleep(500); -// -// stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP); -// -// // Wait for the reset to be processed by the server. -// sleep(500); -// -// AsyncContext asyncContext = asyncContextRef.get(); -// ServletResponse response = asyncContext.getResponse(); -// ServletOutputStream output = response.getOutputStream(); -// -// assertThrows(IOException.class, -// () -> -// { -// // Large writes or explicit flush() must -// // fail because the stream has been reset. -// output.flush(); -// }); -// } // // @Test // public void testStartAsyncThenServerSessionIdleTimeout() throws Exception diff --git a/jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3StreamConnection.java b/jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3StreamConnection.java index 29542da3d1eb..52b0bd715e9d 100644 --- a/jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3StreamConnection.java +++ b/jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3StreamConnection.java @@ -14,7 +14,6 @@ package org.eclipse.jetty.http3; import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; @@ -275,7 +274,7 @@ private void tryReleaseInputBuffer(boolean force) } } - private MessageParser.Result parseAndFill(boolean setFillInterest) + private MessageParser.Result parseAndFill(boolean setFillInterest) throws IOException { try { @@ -336,16 +335,9 @@ private MessageParser.Result parseAndFill(boolean setFillInterest) } } - private int fill(ByteBuffer byteBuffer) + private int fill(ByteBuffer byteBuffer) throws IOException { - try - { - return getEndPoint().fill(byteBuffer); - } - catch (IOException x) - { - throw new UncheckedIOException(x.getMessage(), x); - } + return getEndPoint().fill(byteBuffer); } private void processHeaders(HeadersFrame frame, boolean wasBlocked, Runnable delegate) diff --git a/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpStreamOverHTTP3.java b/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpStreamOverHTTP3.java index 1552659ad97d..33e9828772d8 100644 --- a/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpStreamOverHTTP3.java +++ b/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpStreamOverHTTP3.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.http3.server.internal; +import java.io.EOFException; import java.nio.ByteBuffer; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeoutException; @@ -33,6 +34,7 @@ import org.eclipse.jetty.http3.frames.DataFrame; import org.eclipse.jetty.http3.frames.HeadersFrame; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.EofException; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpStream; import org.eclipse.jetty.server.Request; @@ -536,6 +538,8 @@ public Runnable onFailure(Throwable failure) chunk = Content.Chunk.from(failure, true); } connection.onFailure(failure); - return httpChannel.onFailure(failure); + + boolean remote = failure instanceof EOFException; + return remote ? httpChannel.onRemoteFailure(new EofException(failure)) : httpChannel.onFailure(failure); } } diff --git a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicStreamEndPoint.java b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicStreamEndPoint.java index b8a2a731a63e..227af953034c 100644 --- a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicStreamEndPoint.java +++ b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicStreamEndPoint.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.quic.common; +import java.io.EOFException; import java.io.IOException; import java.net.SocketAddress; import java.nio.ByteBuffer; @@ -41,6 +42,7 @@ public class QuicStreamEndPoint extends AbstractEndPoint { private static final Logger LOG = LoggerFactory.getLogger(QuicStreamEndPoint.class); private static final ByteBuffer LAST_FLAG = ByteBuffer.allocate(0); + private static final ByteBuffer EMPTY_WRITABLE_BUFFER = ByteBuffer.allocate(0); private final QuicSession session; private final long streamId; @@ -265,12 +267,25 @@ public boolean onReadable() } else { - QuicStreamEndPoint streamEndPoint = getQuicSession().getStreamEndPoint(streamId); - if (streamEndPoint.isStreamFinished()) + if (isStreamFinished()) { - EofException e = new EofException(); - streamEndPoint.getFillInterest().onFail(e); - streamEndPoint.getQuicSession().onFailure(e); + // Check if the stream was finished normally. + try + { + fill(EMPTY_WRITABLE_BUFFER); + } + catch (EOFException x) + { + // Got reset. + getFillInterest().onFail(x); + getQuicSession().onFailure(x); + } + catch (Throwable x) + { + EofException e = new EofException(x); + getFillInterest().onFail(e); + getQuicSession().onFailure(e); + } } } return interested; diff --git a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign/src/main/java/org/eclipse/jetty/quic/quiche/foreign/ForeignQuicheConnection.java b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign/src/main/java/org/eclipse/jetty/quic/quiche/foreign/ForeignQuicheConnection.java index 2327aa664948..9aa018d0efe4 100644 --- a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign/src/main/java/org/eclipse/jetty/quic/quiche/foreign/ForeignQuicheConnection.java +++ b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign/src/main/java/org/eclipse/jetty/quic/quiche/foreign/ForeignQuicheConnection.java @@ -14,6 +14,7 @@ package org.eclipse.jetty.quic.quiche.foreign; import java.io.ByteArrayOutputStream; +import java.io.EOFException; import java.io.IOException; import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; @@ -918,14 +919,19 @@ public int drainClearBytesForStream(long streamId, ByteBuffer buffer) throws IOE MemorySegment fin = scope.allocate(NativeHelper.C_CHAR); read = quiche_h.quiche_conn_stream_recv(quicheConn, streamId, bufferSegment, buffer.remaining(), fin); - int prevPosition = buffer.position(); - buffer.put(bufferSegment.asByteBuffer().limit((int)read)); - buffer.position(prevPosition); + if (read > 0) + { + int prevPosition = buffer.position(); + buffer.put(bufferSegment.asByteBuffer().limit((int)read)); + buffer.position(prevPosition); + } } } if (read == quiche_error.QUICHE_ERR_DONE) return isStreamFinished(streamId) ? -1 : 0; + if (read == quiche_error.QUICHE_ERR_STREAM_RESET) + throw new EOFException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read)); if (read < 0L) throw new IOException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read)); buffer.position((int)(buffer.position() + read)); diff --git a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java index d633de77e21d..b81021957a2d 100644 --- a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java +++ b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java @@ -14,6 +14,7 @@ package org.eclipse.jetty.quic.quiche.jna; import java.io.ByteArrayOutputStream; +import java.io.EOFException; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; @@ -747,6 +748,8 @@ public int drainClearBytesForStream(long streamId, ByteBuffer buffer) throws IOE int read = LibQuiche.INSTANCE.quiche_conn_stream_recv(quicheConn, new uint64_t(streamId), buffer, new size_t(buffer.remaining()), fin).intValue(); if (read == quiche_error.QUICHE_ERR_DONE) return isStreamFinished(streamId) ? -1 : 0; + if (read == quiche_error.QUICHE_ERR_STREAM_RESET) + throw new EOFException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read)); if (read < 0L) throw new IOException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read)); buffer.position(buffer.position() + read); diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index a565ccce5b59..61a157ebd9e8 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -88,8 +88,7 @@ public interface HttpChannel extends Invocable /** *

Notifies this {@code HttpChannel} that an asynchronous failure happened.

- *

Typical failure examples could be HTTP/2 resets or - * protocol failures (for example, invalid request bytes).

+ *

Typical failure examples could be protocol failures (for example, invalid request bytes).

* * @param failure the failure cause. * @return a {@code Runnable} that performs the failure action, or {@code null} @@ -98,6 +97,18 @@ public interface HttpChannel extends Invocable */ Runnable onFailure(Throwable failure); + /** + *

Notifies this {@code HttpChannel} that an asynchronous notification was received indicating + * a remote failure happened.

+ *

Typical failure examples could be HTTP/2 resets.

+ * + * @param failure the failure cause. + * @return a {@code Runnable} that performs the failure action, or {@code null} + * if no failure action needs be performed by the calling thread + * @see Request#addFailureListener(Consumer) + */ + Runnable onRemoteFailure(Throwable failure); + /** *

Notifies this {@code HttpChannel} that an asynchronous close happened.

* diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java index dc2b3be55d62..e97331b573ae 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java @@ -392,6 +392,17 @@ public Runnable onIdleTimeout(TimeoutException t) @Override public Runnable onFailure(Throwable x) + { + return onFailure(x, false); + } + + @Override + public Runnable onRemoteFailure(Throwable x) + { + return onFailure(x, true); + } + + private Runnable onFailure(Throwable x, boolean remote) { HttpStream stream; Runnable task; @@ -437,7 +448,9 @@ public Runnable onFailure(Throwable x) // Notify the failure listeners only once. Consumer onFailure = _onFailure; _onFailure = null; - Runnable invokeOnFailureListeners = onFailure == null ? null : () -> + + boolean skipListeners = remote && !getHttpConfiguration().isNotifyRemoteAsyncErrors(); + Runnable invokeOnFailureListeners = onFailure == null || skipListeners ? null : () -> { try { diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java index 419fc0d9ebf5..b9e6270401d7 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java @@ -676,7 +676,9 @@ public void flush() throws IOException catch (Throwable t) { onWriteComplete(false, t); - throw t; + if (t instanceof IOException) + throw t; + throw new IOException(t); } } } diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/Http2AsyncIOServletTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/Http2AsyncIOServletTest.java new file mode 100644 index 000000000000..c55aadae64c9 --- /dev/null +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/Http2AsyncIOServletTest.java @@ -0,0 +1,151 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.ee10.test.client.transport; + +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.ErrorCode; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.frames.ResetFrame; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.FuturePromise; +import org.eclipse.jetty.util.component.LifeCycle; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class Http2AsyncIOServletTest +{ + private Server server; + private ServerConnector connector; + private HTTP2Client client; + + private void start(HttpConfiguration httpConfig, HttpServlet httpServlet) throws Exception + { + server = new Server(); + connector = new ServerConnector(server, 1, 1, new HTTP2CServerConnectionFactory(httpConfig)); + server.addConnector(connector); + ServletContextHandler servletContextHandler = new ServletContextHandler("/"); + servletContextHandler.addServlet(new ServletHolder(httpServlet), "/*"); + server.setHandler(servletContextHandler); + server.start(); + + client = new HTTP2Client(); + client.start(); + } + + @AfterEach + public void tearDown() + { + LifeCycle.stop(client); + LifeCycle.stop(server); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testStartAsyncThenClientResetRemoteErrorNotification(boolean notify) throws Exception + { + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setNotifyRemoteAsyncErrors(notify); + + AtomicReference errorAsyncEventRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + start(httpConfig, new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + { + AsyncContext asyncContext = request.startAsync(); + asyncContext.addListener(new AsyncListener() + { + @Override + public void onComplete(AsyncEvent event) + { + } + + @Override + public void onTimeout(AsyncEvent event) + { + } + + @Override + public void onError(AsyncEvent event) + { + errorAsyncEventRef.set(event); + asyncContext.complete(); + } + + @Override + public void onStartAsync(AsyncEvent event) + { + } + }); + asyncContext.setTimeout(0); + latch.countDown(); + } + }); + + InetSocketAddress address = new InetSocketAddress("localhost", connector.getLocalPort()); + FuturePromise sessionPromise = new FuturePromise<>(); + client.connect(address, new Session.Listener() {}, sessionPromise); + Session session = sessionPromise.get(5, TimeUnit.SECONDS); + MetaData.Request metaData = new MetaData.Request("GET", HttpURI.from("/"), HttpVersion.HTTP_2, HttpFields.EMPTY); + HeadersFrame frame = new HeadersFrame(metaData, null, false); + Stream stream = session.newStream(frame, null).get(5, TimeUnit.SECONDS); + + // Wait for the server to be in ASYNC_WAIT. + assertTrue(latch.await(5, TimeUnit.SECONDS)); + Thread.sleep(500); + + stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code)); + + if (notify) + // Wait for the reset to be notified to the async context listener. + await().atMost(5, TimeUnit.SECONDS).until(() -> + { + AsyncEvent asyncEvent = errorAsyncEventRef.get(); + return asyncEvent == null ? null : asyncEvent.getThrowable(); + }, instanceOf(EofException.class)); + else + // Wait for the reset to NOT be notified to the failure listener. + await().atMost(5, TimeUnit.SECONDS).during(1, TimeUnit.SECONDS).until(errorAsyncEventRef::get, nullValue()); + } +} diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/Http3AsyncIOServletTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/Http3AsyncIOServletTest.java new file mode 100644 index 000000000000..64d5eaf00bbc --- /dev/null +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/Http3AsyncIOServletTest.java @@ -0,0 +1,161 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.ee10.test.client.transport; + +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http3.HTTP3ErrorCode; +import org.eclipse.jetty.http3.api.Stream; +import org.eclipse.jetty.http3.client.HTTP3Client; +import org.eclipse.jetty.http3.frames.HeadersFrame; +import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.toolchain.test.MavenPaths; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.awaitility.Awaitility.await; +import static org.eclipse.jetty.http3.api.Session.Client; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(WorkDirExtension.class) +public class Http3AsyncIOServletTest +{ + public WorkDir workDir; + + private Server server; + private QuicServerConnector connector; + private HTTP3Client client; + + private void start(HttpConfiguration httpConfig, HttpServlet httpServlet) throws Exception + { + server = new Server(); + SslContextFactory.Server serverSslContextFactory = new SslContextFactory.Server(); + serverSslContextFactory.setKeyStorePath(MavenPaths.findTestResourceFile("keystore.p12").toString()); + serverSslContextFactory.setKeyStorePassword("storepwd"); + ServerQuicConfiguration serverQuicConfiguration = new ServerQuicConfiguration(serverSslContextFactory, workDir.getEmptyPathDir()); + connector = new QuicServerConnector(server, serverQuicConfiguration, new HTTP3ServerConnectionFactory(serverQuicConfiguration, httpConfig)); + server.addConnector(connector); + ServletContextHandler servletContextHandler = new ServletContextHandler("/"); + servletContextHandler.addServlet(new ServletHolder(httpServlet), "/*"); + server.setHandler(servletContextHandler); + server.start(); + + client = new HTTP3Client(new ClientQuicConfiguration(new SslContextFactory.Client(true), null)); + client.start(); + } + + @AfterEach + public void tearDown() + { + LifeCycle.stop(client); + LifeCycle.stop(server); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testStartAsyncThenClientResetRemoteErrorNotification(boolean notify) throws Exception + { + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setNotifyRemoteAsyncErrors(notify); + + AtomicReference errorAsyncEventRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + start(httpConfig, new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + { + AsyncContext asyncContext = request.startAsync(); + asyncContext.addListener(new AsyncListener() + { + @Override + public void onComplete(AsyncEvent event) + { + } + + @Override + public void onTimeout(AsyncEvent event) + { + } + + @Override + public void onError(AsyncEvent event) + { + errorAsyncEventRef.set(event); + asyncContext.complete(); + } + + @Override + public void onStartAsync(AsyncEvent event) + { + } + }); + asyncContext.setTimeout(0); + latch.countDown(); + } + }); + + InetSocketAddress address = new InetSocketAddress("localhost", connector.getLocalPort()); + Client session = client.connect(address, new Client.Listener() {}).get(5, TimeUnit.SECONDS); + MetaData.Request metaData = new MetaData.Request("GET", HttpURI.from("/"), HttpVersion.HTTP_3, HttpFields.EMPTY); + HeadersFrame frame = new HeadersFrame(metaData, false); + Stream stream = session.newRequest(frame, null).get(5, TimeUnit.SECONDS); + + // Wait for the server to be in ASYNC_WAIT. + assertTrue(latch.await(5, TimeUnit.SECONDS)); + Thread.sleep(500); + + stream.reset(HTTP3ErrorCode.REQUEST_CANCELLED_ERROR.code(), new Exception()); + + if (notify) + // Wait for the reset to be notified to the async context listener. + await().atMost(5, TimeUnit.SECONDS).until(() -> + { + AsyncEvent asyncEvent = errorAsyncEventRef.get(); + return asyncEvent == null ? null : asyncEvent.getThrowable(); + }, instanceOf(EofException.class)); + else + // Wait for the reset to NOT be notified to the failure listener. + await().atMost(5, TimeUnit.SECONDS).during(1, TimeUnit.SECONDS).until(errorAsyncEventRef::get, nullValue()); + } +} diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java index c69bb3664108..b326486e504e 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java @@ -740,7 +740,9 @@ public void flush() throws IOException catch (Throwable t) { onWriteComplete(false, t); - throw t; + if (t instanceof IOException) + throw t; + throw new IOException(t); } } } diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/pom.xml b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/pom.xml index e98c0af0c962..4a28e6e5dfd9 100644 --- a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/pom.xml +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/pom.xml @@ -112,7 +112,7 @@ @{argLine} ${jetty.surefire.argLine} - --enable-native-access org.eclipse.jetty.quic.quiche.foreign + --enable-native-access=ALL-UNNAMED diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/Http2AsyncIOServletTest.java b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/Http2AsyncIOServletTest.java new file mode 100644 index 000000000000..12e6a7cfef6c --- /dev/null +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/Http2AsyncIOServletTest.java @@ -0,0 +1,150 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.ee9.test.client.transport; + +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.ee9.servlet.ServletContextHandler; +import org.eclipse.jetty.ee9.servlet.ServletHolder; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.ErrorCode; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.frames.ResetFrame; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.FuturePromise; +import org.eclipse.jetty.util.component.LifeCycle; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class Http2AsyncIOServletTest +{ + private Server server; + private ServerConnector connector; + private HTTP2Client client; + + private void start(HttpConfiguration httpConfig, HttpServlet httpServlet) throws Exception + { + server = new Server(); + connector = new ServerConnector(server, 1, 1, new HTTP2CServerConnectionFactory(httpConfig)); + server.addConnector(connector); + ServletContextHandler servletContextHandler = new ServletContextHandler(server, "/"); + servletContextHandler.addServlet(new ServletHolder(httpServlet), "/*"); + server.start(); + + client = new HTTP2Client(); + client.start(); + } + + @AfterEach + public void tearDown() + { + LifeCycle.stop(client); + LifeCycle.stop(server); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testStartAsyncThenClientResetRemoteErrorNotification(boolean notify) throws Exception + { + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setNotifyRemoteAsyncErrors(notify); + + AtomicReference errorAsyncEventRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + start(httpConfig, new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + { + AsyncContext asyncContext = request.startAsync(); + asyncContext.addListener(new AsyncListener() + { + @Override + public void onComplete(AsyncEvent event) + { + } + + @Override + public void onTimeout(AsyncEvent event) + { + } + + @Override + public void onError(AsyncEvent event) + { + errorAsyncEventRef.set(event); + asyncContext.complete(); + } + + @Override + public void onStartAsync(AsyncEvent event) + { + } + }); + asyncContext.setTimeout(0); + latch.countDown(); + } + }); + + InetSocketAddress address = new InetSocketAddress("localhost", connector.getLocalPort()); + FuturePromise sessionPromise = new FuturePromise<>(); + client.connect(address, new Session.Listener() {}, sessionPromise); + Session session = sessionPromise.get(5, TimeUnit.SECONDS); + MetaData.Request metaData = new MetaData.Request("GET", HttpURI.from("/"), HttpVersion.HTTP_2, HttpFields.EMPTY); + HeadersFrame frame = new HeadersFrame(metaData, null, false); + Stream stream = session.newStream(frame, null).get(5, TimeUnit.SECONDS); + + // Wait for the server to be in ASYNC_WAIT. + assertTrue(latch.await(5, TimeUnit.SECONDS)); + Thread.sleep(500); + + stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code)); + + if (notify) + // Wait for the reset to be notified to the async context listener. + await().atMost(5, TimeUnit.SECONDS).until(() -> + { + AsyncEvent asyncEvent = errorAsyncEventRef.get(); + return asyncEvent == null ? null : asyncEvent.getThrowable(); + }, instanceOf(EofException.class)); + else + // Wait for the reset to NOT be notified to the failure listener. + await().atMost(5, TimeUnit.SECONDS).during(1, TimeUnit.SECONDS).until(errorAsyncEventRef::get, nullValue()); + } +} diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/Http3AsyncIOServletTest.java b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/Http3AsyncIOServletTest.java new file mode 100644 index 000000000000..737e29e4dd03 --- /dev/null +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/Http3AsyncIOServletTest.java @@ -0,0 +1,161 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.ee9.test.client.transport; + +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.ee9.servlet.ServletContextHandler; +import org.eclipse.jetty.ee9.servlet.ServletHolder; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http3.HTTP3ErrorCode; +import org.eclipse.jetty.http3.api.Session; +import org.eclipse.jetty.http3.api.Stream; +import org.eclipse.jetty.http3.client.HTTP3Client; +import org.eclipse.jetty.http3.frames.HeadersFrame; +import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.toolchain.test.MavenPaths; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.awaitility.Awaitility.await; +import static org.eclipse.jetty.http3.api.Session.Client; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(WorkDirExtension.class) +public class Http3AsyncIOServletTest +{ + public WorkDir workDir; + + private Server server; + private QuicServerConnector connector; + private HTTP3Client client; + + private void start(HttpConfiguration httpConfig, HttpServlet httpServlet) throws Exception + { + server = new Server(); + SslContextFactory.Server serverSslContextFactory = new SslContextFactory.Server(); + serverSslContextFactory.setKeyStorePath(MavenPaths.findTestResourceFile("keystore.p12").toString()); + serverSslContextFactory.setKeyStorePassword("storepwd"); + ServerQuicConfiguration serverQuicConfiguration = new ServerQuicConfiguration(serverSslContextFactory, workDir.getEmptyPathDir()); + connector = new QuicServerConnector(server, serverQuicConfiguration, new HTTP3ServerConnectionFactory(serverQuicConfiguration, httpConfig)); + server.addConnector(connector); + ServletContextHandler servletContextHandler = new ServletContextHandler(server, "/"); + servletContextHandler.addServlet(new ServletHolder(httpServlet), "/*"); + server.start(); + + client = new HTTP3Client(new ClientQuicConfiguration(new SslContextFactory.Client(true), null)); + client.start(); + } + + @AfterEach + public void tearDown() + { + LifeCycle.stop(client); + LifeCycle.stop(server); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testStartAsyncThenClientResetRemoteErrorNotification(boolean notify) throws Exception + { + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setNotifyRemoteAsyncErrors(notify); + + AtomicReference errorAsyncEventRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + start(httpConfig, new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + { + AsyncContext asyncContext = request.startAsync(); + asyncContext.addListener(new AsyncListener() + { + @Override + public void onComplete(AsyncEvent event) + { + } + + @Override + public void onTimeout(AsyncEvent event) + { + } + + @Override + public void onError(AsyncEvent event) + { + errorAsyncEventRef.set(event); + asyncContext.complete(); + } + + @Override + public void onStartAsync(AsyncEvent event) + { + } + }); + asyncContext.setTimeout(0); + latch.countDown(); + } + }); + + InetSocketAddress address = new InetSocketAddress("localhost", connector.getLocalPort()); + Session.Client session = client.connect(address, new Client.Listener() {}).get(5, TimeUnit.SECONDS); + MetaData.Request metaData = new MetaData.Request("GET", HttpURI.from("/"), HttpVersion.HTTP_3, HttpFields.EMPTY); + HeadersFrame frame = new HeadersFrame(metaData, false); + Stream stream = session.newRequest(frame, null).get(5, TimeUnit.SECONDS); + + // Wait for the server to be in ASYNC_WAIT. + assertTrue(latch.await(5, TimeUnit.SECONDS)); + Thread.sleep(500); + + stream.reset(HTTP3ErrorCode.REQUEST_CANCELLED_ERROR.code(), new Exception()); + + if (notify) + // Wait for the reset to be notified to the async context listener. + await().atMost(5, TimeUnit.SECONDS).until(() -> + { + AsyncEvent asyncEvent = errorAsyncEventRef.get(); + return asyncEvent == null ? null : asyncEvent.getThrowable(); + }, instanceOf(EofException.class)); + else + // Wait for the reset to NOT be notified to the failure listener. + await().atMost(5, TimeUnit.SECONDS).during(1, TimeUnit.SECONDS).until(errorAsyncEventRef::get, nullValue()); + } +}