diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index 001b88fbfeb..edf9a20139a 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -578,6 +578,17 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu return _contextPathEncoded; } + /** + * Get the context path in a form suitable to be returned from {@link HttpServletRequest#getContextPath()} + * or {@link ServletContext#getContextPath()}. + * @return Returns the encoded contextPath, or empty string for root context + */ + public String getRequestContextPath() + { + String contextPathEncoded = getContextPathEncoded(); + return "/".equals(contextPathEncoded) ? "" : contextPathEncoded; + } + /* * @see javax.servlet.ServletContext#getInitParameter(java.lang.String) */ @@ -2329,10 +2340,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu @Override public String getContextPath() { - if ((_contextPath != null) && _contextPath.equals(URIUtil.SLASH)) - return ""; - - return _contextPath; + return getRequestContextPath(); } @Override diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/IncludedServletTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/IncludedServletTest.java index 1401c6c6272..d82bc64032f 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/IncludedServletTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/IncludedServletTest.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.servlet; +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -25,8 +26,12 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.net.HttpURLConnection; import java.net.URI; +import java.util.ArrayList; +import java.util.List; import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -34,6 +39,7 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.toolchain.test.IO; +import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -71,6 +77,45 @@ public class IncludedServletTest } } + public static class IncludedAttrServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + if (req.getDispatcherType() == DispatcherType.INCLUDE) + { + if (req.getAttribute("included") == null) + { + req.setAttribute("included", Boolean.TRUE); + dumpAttrs("BEFORE1", req, resp.getOutputStream()); + req.getRequestDispatcher("two").include(req, resp); + dumpAttrs("AFTER1", req, resp.getOutputStream()); + } + else + { + dumpAttrs("DURING", req, resp.getOutputStream()); + } + } + else + { + resp.setContentType("text/plain"); + dumpAttrs("BEFORE0", req, resp.getOutputStream()); + req.getRequestDispatcher("one").include(req, resp); + dumpAttrs("AFTER0", req, resp.getOutputStream()); + } + } + + private void dumpAttrs(String tag, HttpServletRequest req, ServletOutputStream out) throws IOException + { + out.println(String.format("%s: %s='%s'", tag, RequestDispatcher.INCLUDE_CONTEXT_PATH, + req.getAttribute(RequestDispatcher.INCLUDE_CONTEXT_PATH))); + out.println(String.format("%s: %s='%s'", tag, RequestDispatcher.INCLUDE_SERVLET_PATH, + req.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH))); + out.println(String.format("%s: %s='%s'", tag, RequestDispatcher.INCLUDE_PATH_INFO, + req.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO))); + } + } + private Server server; private URI baseUri; @@ -87,6 +132,7 @@ public class IncludedServletTest context.setContextPath("/"); context.addServlet(TopServlet.class, "/top"); context.addServlet(IncludedServlet.class, "/included"); + context.addServlet(IncludedAttrServlet.class, "/attr/*"); server.setHandler(context); @@ -173,4 +219,52 @@ public class IncludedServletTest IO.close(in); } } + + @Test + public void testIncludeAttributes() throws IOException + { + URI uri = baseUri.resolve("/attr/one"); + InputStream in = null; + BufferedReader reader = null; + HttpURLConnection connection = null; + + try + { + connection = (HttpURLConnection)uri.toURL().openConnection(); + connection.connect(); + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK)); + in = connection.getInputStream(); + reader = new BufferedReader(new InputStreamReader(in)); + List result = new ArrayList<>(); + String line = reader.readLine(); + while (line != null) + { + result.add(line); + line = reader.readLine(); + } + + assertThat(result, Matchers.contains( + "BEFORE0: javax.servlet.include.context_path='null'", + "BEFORE0: javax.servlet.include.servlet_path='null'", + "BEFORE0: javax.servlet.include.path_info='null'", + "BEFORE1: javax.servlet.include.context_path=''", + "BEFORE1: javax.servlet.include.servlet_path='/attr'", + "BEFORE1: javax.servlet.include.path_info='/one'", + "DURING: javax.servlet.include.context_path=''", + "DURING: javax.servlet.include.servlet_path='/attr'", + "DURING: javax.servlet.include.path_info='/two'", + "AFTER1: javax.servlet.include.context_path=''", + "AFTER1: javax.servlet.include.servlet_path='/attr'", + "AFTER1: javax.servlet.include.path_info='/one'", + "AFTER0: javax.servlet.include.context_path='null'", + "AFTER0: javax.servlet.include.servlet_path='null'", + "AFTER0: javax.servlet.include.path_info='null'" + )); + } + finally + { + IO.close(reader); + IO.close(in); + } + } } diff --git a/jetty-websocket/websocket-core-client/src/main/java/org/eclipse/jetty/websocket/core/client/ClientUpgradeRequest.java b/jetty-websocket/websocket-core-client/src/main/java/org/eclipse/jetty/websocket/core/client/ClientUpgradeRequest.java index 5ce26793ba6..0f6ae204d5e 100644 --- a/jetty-websocket/websocket-core-client/src/main/java/org/eclipse/jetty/websocket/core/client/ClientUpgradeRequest.java +++ b/jetty-websocket/websocket-core-client/src/main/java/org/eclipse/jetty/websocket/core/client/ClientUpgradeRequest.java @@ -111,6 +111,11 @@ public abstract class ClientUpgradeRequest extends HttpRequest implements Respon this.wsClient = webSocketClient; this.futureCoreSession = new CompletableFuture<>(); + this.futureCoreSession.whenComplete((session, throwable) -> + { + if (throwable != null) + abort(throwable); + }); } public void setConfiguration(Configuration.ConfigurationCustomizer config) diff --git a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketSession.java b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketSession.java index 6da55c40efe..526a7ff97bf 100644 --- a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketSession.java +++ b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketSession.java @@ -93,6 +93,7 @@ public class JavaxWebSocketSession implements javax.websocket.Session } this.userProperties = endpointConfig.getUserProperties(); + container.notifySessionListeners((listener) -> listener.onJavaxWebSocketSessionCreated(this)); } public CoreSession getCoreSession() diff --git a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketSessionListener.java b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketSessionListener.java index 6538fcd5a60..f458acef62d 100644 --- a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketSessionListener.java +++ b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketSessionListener.java @@ -20,7 +20,15 @@ package org.eclipse.jetty.websocket.javax.common; public interface JavaxWebSocketSessionListener { - void onJavaxWebSocketSessionOpened(JavaxWebSocketSession session); + default void onJavaxWebSocketSessionCreated(JavaxWebSocketSession session) + { + } - void onJavaxWebSocketSessionClosed(JavaxWebSocketSession session); + default void onJavaxWebSocketSessionOpened(JavaxWebSocketSession session) + { + } + + default void onJavaxWebSocketSessionClosed(JavaxWebSocketSession session) + { + } } diff --git a/jetty-websocket/websocket-jetty-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketSessionListener.java b/jetty-websocket/websocket-jetty-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketSessionListener.java index 22d13152b77..8a809f99708 100644 --- a/jetty-websocket/websocket-jetty-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketSessionListener.java +++ b/jetty-websocket/websocket-jetty-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketSessionListener.java @@ -23,6 +23,10 @@ package org.eclipse.jetty.websocket.api; */ public interface WebSocketSessionListener { + default void onWebSocketSessionCreated(Session session) + { + } + default void onWebSocketSessionOpened(Session session) { } diff --git a/jetty-websocket/websocket-jetty-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java b/jetty-websocket/websocket-jetty-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java index ed36d8a0906..dc5588d37c2 100644 --- a/jetty-websocket/websocket-jetty-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java +++ b/jetty-websocket/websocket-jetty-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java @@ -50,6 +50,7 @@ import org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandler; import org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandlerFactory; import org.eclipse.jetty.websocket.common.SessionTracker; import org.eclipse.jetty.websocket.core.Configuration; +import org.eclipse.jetty.websocket.core.CoreSession; import org.eclipse.jetty.websocket.core.WebSocketComponents; import org.eclipse.jetty.websocket.core.client.UpgradeListener; import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; @@ -151,7 +152,8 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli } CompletableFuture futureSession = new CompletableFuture<>(); - coreClient.connect(upgradeRequest).whenComplete((coreSession, error) -> + CompletableFuture coreConnect = coreClient.connect(upgradeRequest); + coreConnect.whenComplete((coreSession, error) -> { if (error != null) { @@ -163,6 +165,12 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli futureSession.complete(frameHandler.getSession()); }); + // If the returned future is cancelled we want to try to cancel the core future if possible. + futureSession.whenComplete((session, throwable) -> + { + if (throwable != null) + coreConnect.completeExceptionally(throwable); + }); return futureSession; } diff --git a/jetty-websocket/websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandler.java b/jetty-websocket/websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandler.java index dee0c4f033c..c2b8c5bee96 100644 --- a/jetty-websocket/websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandler.java +++ b/jetty-websocket/websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandler.java @@ -153,7 +153,7 @@ public class JettyWebSocketFrameHandler implements FrameHandler try { customizer.customize(coreSession); - session = new WebSocketSession(coreSession, this); + session = new WebSocketSession(container, coreSession, this); frameHandle = InvokerUtils.bindTo(frameHandle, session); openHandle = InvokerUtils.bindTo(openHandle, session); diff --git a/jetty-websocket/websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSession.java b/jetty-websocket/websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSession.java index 6f1c2a5988a..50ef74140f3 100644 --- a/jetty-websocket/websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSession.java +++ b/jetty-websocket/websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSession.java @@ -30,6 +30,7 @@ import org.eclipse.jetty.websocket.api.SuspendToken; import org.eclipse.jetty.websocket.api.UpgradeRequest; import org.eclipse.jetty.websocket.api.UpgradeResponse; import org.eclipse.jetty.websocket.api.WebSocketBehavior; +import org.eclipse.jetty.websocket.api.WebSocketContainer; import org.eclipse.jetty.websocket.core.CoreSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,13 +44,14 @@ public class WebSocketSession implements Session, SuspendToken, Dumpable private final UpgradeRequest upgradeRequest; private final UpgradeResponse upgradeResponse; - public WebSocketSession(CoreSession coreSession, JettyWebSocketFrameHandler frameHandler) + public WebSocketSession(WebSocketContainer container, CoreSession coreSession, JettyWebSocketFrameHandler frameHandler) { this.frameHandler = Objects.requireNonNull(frameHandler); this.coreSession = Objects.requireNonNull(coreSession); this.upgradeRequest = frameHandler.getUpgradeRequest(); this.upgradeResponse = frameHandler.getUpgradeResponse(); this.remoteEndpoint = new JettyWebSocketRemoteEndpoint(coreSession, frameHandler.getBatchMode()); + container.notifySessionListeners((listener) -> listener.onWebSocketSessionCreated(this)); } @Override diff --git a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServerContainer.java b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServerContainer.java index bc6c85030e8..ed0c40b07fa 100644 --- a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServerContainer.java +++ b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServerContainer.java @@ -42,6 +42,7 @@ import org.eclipse.jetty.websocket.core.exception.WebSocketException; import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents; import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.eclipse.jetty.websocket.server.internal.JettyServerFrameHandlerFactory; +import org.eclipse.jetty.websocket.util.ReflectUtils; import org.eclipse.jetty.websocket.util.server.internal.FrameHandlerFactory; import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping; import org.slf4j.Logger; @@ -131,6 +132,24 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements frameHandlerFactory, customizer); } + public void addMapping(String pathSpec, final Class endpointClass) + { + if (!ReflectUtils.isDefaultConstructable(endpointClass)) + throw new IllegalArgumentException("Cannot access default constructor for the class: " + endpointClass.getName()); + + addMapping(pathSpec, (req, resp) -> + { + try + { + return endpointClass.getDeclaredConstructor().newInstance(); + } + catch (Exception e) + { + throw new org.eclipse.jetty.websocket.api.WebSocketException("Unable to create instance of " + endpointClass.getName(), e); + } + }); + } + @Override public Executor getExecutor() { diff --git a/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ConnectFutureTest.java b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ConnectFutureTest.java new file mode 100644 index 00000000000..af79a4d3913 --- /dev/null +++ b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ConnectFutureTest.java @@ -0,0 +1,401 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://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: +// the Apache License v2.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.websocket.tests.client; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; + +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.HttpResponse; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.StatusCode; +import org.eclipse.jetty.websocket.api.UpgradeException; +import org.eclipse.jetty.websocket.api.WebSocketException; +import org.eclipse.jetty.websocket.api.WebSocketSessionListener; +import org.eclipse.jetty.websocket.api.util.WSURI; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.JettyUpgradeListener; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer; +import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint; +import org.eclipse.jetty.websocket.tests.EchoSocket; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Disabled() // TODO: merge changes from PR #5030 properly. +public class ConnectFutureTest +{ + private Server server; + private WebSocketClient client; + + public void start(Consumer configuration) throws Exception + { + server = new Server(); + ServerConnector connector = new ServerConnector(server); + server.addConnector(connector); + + ServletContextHandler contextHandler = new ServletContextHandler(); + contextHandler.setContextPath("/"); + server.setHandler(contextHandler); + + JettyWebSocketServletContainerInitializer.configure(contextHandler, (context, container) -> + configuration.accept(container)); + server.start(); + + client = new WebSocketClient(); + client.start(); + } + + @AfterEach + public void stop() throws Exception + { + if (client != null) + client.stop(); + if (server != null) + server.stop(); + } + + @Test + public void test() + { + CompletableFuture future1 = new CompletableFuture<>(); + future1.whenComplete((s, t) -> System.err.println(String.format("future1{%s,%s}", s, t))); + + CompletableFuture future2 = new CompletableFuture<>(); + future1.whenComplete((s, t) -> + { + if (t != null) + { + future1.completeExceptionally(t); + return; + } + + future1.complete(s); + }); + + + } + + @Test + public void testAbortDuringCreator() throws Exception + { + CountDownLatch enteredCreator = new CountDownLatch(1); + CountDownLatch exitCreator = new CountDownLatch(1); + start(c -> + { + c.addMapping("/", (req, res) -> + { + try + { + enteredCreator.countDown(); + exitCreator.await(); + return new EchoSocket(); + } + catch (InterruptedException e) + { + throw new IllegalStateException(e); + } + }); + }); + + CloseTrackingEndpoint clientSocket = new CloseTrackingEndpoint(); + Future connect = client.connect(clientSocket, WSURI.toWebsocket(server.getURI())); + + // Cancel the future once we have entered the servers WebSocketCreator (after upgrade request is sent). + assertTrue(enteredCreator.await(5, TimeUnit.SECONDS)); + assertTrue(connect.cancel(true)); + assertThrows(CancellationException.class, () -> connect.get(5, TimeUnit.SECONDS)); + exitCreator.countDown(); + assertFalse(clientSocket.openLatch.await(1, TimeUnit.SECONDS)); + + Throwable error = clientSocket.error.get(); + assertThat(error, instanceOf(UpgradeException.class)); + Throwable cause = error.getCause(); + assertThat(cause, instanceOf(org.eclipse.jetty.websocket.core.exception.UpgradeException.class)); + assertThat(cause.getCause(), instanceOf(CancellationException.class)); + } + + @Test + public void testAbortSessionOnCreated() throws Exception + { + start(c -> c.addMapping("/", EchoSocket.class)); + + CountDownLatch enteredListener = new CountDownLatch(1); + CountDownLatch exitListener = new CountDownLatch(1); + client.addSessionListener(new WebSocketSessionListener() + { + @Override + public void onWebSocketSessionCreated(Session session) + { + try + { + enteredListener.countDown(); + exitListener.await(); + } + catch (InterruptedException e) + { + throw new IllegalStateException(e); + } + } + }); + + CloseTrackingEndpoint clientSocket = new CloseTrackingEndpoint(); + Future connect = client.connect(clientSocket, WSURI.toWebsocket(server.getURI())); + + // Abort when session is created, this is before future has been added to session and before the connection upgrade. + assertTrue(enteredListener.await(5, TimeUnit.SECONDS)); + assertTrue(connect.cancel(true)); + assertThrows(CancellationException.class, () -> connect.get(5, TimeUnit.SECONDS)); + exitListener.countDown(); + assertFalse(clientSocket.openLatch.await(1, TimeUnit.SECONDS)); + assertThat(clientSocket.error.get(), instanceOf(CancellationException.class)); + } + + @Test + public void testAbortInHandshakeResponse() throws Exception + { + start(c -> c.addMapping("/", EchoSocket.class)); + + CountDownLatch enteredListener = new CountDownLatch(1); + CountDownLatch exitListener = new CountDownLatch(1); + JettyUpgradeListener upgradeListener = new JettyUpgradeListener() + { + @Override + public void onHandshakeResponse(HttpRequest request, HttpResponse response) + { + try + { + enteredListener.countDown(); + exitListener.await(); + } + catch (InterruptedException e) + { + throw new IllegalStateException(e); + } + } + }; + + CloseTrackingEndpoint clientSocket = new CloseTrackingEndpoint(); + ClientUpgradeRequest upgradeRequest = new ClientUpgradeRequest(); + Future connect = client.connect(clientSocket, WSURI.toWebsocket(server.getURI()), upgradeRequest, upgradeListener); + + // Abort after after handshake response, which is before connection upgrade, but after future has been set on session. + assertTrue(enteredListener.await(5, TimeUnit.SECONDS)); + assertTrue(connect.cancel(true)); + assertThrows(CancellationException.class, () -> connect.get(5, TimeUnit.SECONDS)); + exitListener.countDown(); + assertFalse(clientSocket.openLatch.await(1, TimeUnit.SECONDS)); + assertThat(clientSocket.error.get(), instanceOf(CancellationException.class)); + } + + @Test + public void testAbortOnOpened() throws Exception + { + start(c -> c.addMapping("/", EchoSocket.class)); + + CountDownLatch exitOnOpen = new CountDownLatch(1); + CloseTrackingEndpoint clientSocket = new CloseTrackingEndpoint() + { + @Override + public void onWebSocketConnect(Session session) + { + try + { + super.onWebSocketConnect(session); + exitOnOpen.await(); + } + catch (InterruptedException e) + { + throw new IllegalStateException(e); + } + } + }; + + // Abort during the call to onOpened. This is after future has been added to session, + // and after connection has been upgraded, but before future completion. + Future connect = client.connect(clientSocket, WSURI.toWebsocket(server.getURI())); + assertTrue(clientSocket.openLatch.await(5, TimeUnit.SECONDS)); + assertTrue(connect.cancel(true)); + exitOnOpen.countDown(); + + // We got an error on the WebSocket endpoint and an error from the future. + assertTrue(clientSocket.errorLatch.await(5, TimeUnit.SECONDS)); + assertThrows(CancellationException.class, () -> connect.get(5, TimeUnit.SECONDS)); + } + + @Test + public void testAbortAfterCompletion() throws Exception + { + start(c -> c.addMapping("/", EchoSocket.class)); + + CloseTrackingEndpoint clientSocket = new CloseTrackingEndpoint(); + Future connect = client.connect(clientSocket, WSURI.toWebsocket(server.getURI())); + Session session = connect.get(5, TimeUnit.SECONDS); + + // If we can send and receive messages the future has been completed. + assertTrue(clientSocket.openLatch.await(5, TimeUnit.SECONDS)); + clientSocket.getSession().getRemote().sendString("hello"); + assertThat(clientSocket.messageQueue.poll(5, TimeUnit.SECONDS), Matchers.is("hello")); + + // After it has been completed we should not get any errors from cancelling it. + assertFalse(connect.cancel(true)); + assertThat(connect.get(5, TimeUnit.SECONDS), instanceOf(Session.class)); + assertFalse(clientSocket.closeLatch.await(1, TimeUnit.SECONDS)); + assertNull(clientSocket.error.get()); + + // Close the session properly. + session.close(); + assertTrue(clientSocket.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(clientSocket.closeCode, is(StatusCode.NORMAL)); + } + + @Test + public void testFutureTimeout() throws Exception + { + CountDownLatch exitCreator = new CountDownLatch(1); + start(c -> + { + c.addMapping("/", (req, res) -> + { + try + { + exitCreator.await(); + return new EchoSocket(); + } + catch (InterruptedException e) + { + throw new IllegalStateException(e); + } + }); + }); + + CloseTrackingEndpoint clientSocket = new CloseTrackingEndpoint(); + Future connect = client.connect(clientSocket, WSURI.toWebsocket(server.getURI())); + assertThrows(TimeoutException.class, () -> connect.get(1, TimeUnit.SECONDS)); + exitCreator.countDown(); + Session session = connect.get(5, TimeUnit.SECONDS); + + // Close the session properly. + session.close(); + assertTrue(clientSocket.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(clientSocket.closeCode, is(StatusCode.NORMAL)); + } + + @Test + public void testAbortWithExceptionBeforeUpgrade() throws Exception + { + CountDownLatch exitCreator = new CountDownLatch(1); + start(c -> + { + c.addMapping("/", (req, res) -> + { + try + { + exitCreator.await(); + return new EchoSocket(); + } + catch (InterruptedException e) + { + throw new IllegalStateException(e); + } + }); + }); + + // Complete the CompletableFuture with an exception the during the call to onOpened. + CloseTrackingEndpoint clientSocket = new CloseTrackingEndpoint(); + CompletableFuture connect = client.connect(clientSocket, WSURI.toWebsocket(server.getURI())); + assertTrue(connect.completeExceptionally(new WebSocketException("custom exception"))); + exitCreator.countDown(); + + // Exception from the future is correct. + ExecutionException futureError = assertThrows(ExecutionException.class, () -> connect.get(5, TimeUnit.SECONDS)); + Throwable cause = futureError.getCause(); + assertThat(cause, instanceOf(WebSocketException.class)); + assertThat(cause.getMessage(), is("custom exception")); + + // Exception from the endpoint is correct. + assertTrue(clientSocket.errorLatch.await(5, TimeUnit.SECONDS)); + Throwable endpointError = clientSocket.error.get(); + assertThat(endpointError, instanceOf(UpgradeException.class)); + Throwable endpointErrorCause = endpointError.getCause(); + assertThat(endpointError, instanceOf(WebSocketException.class)); + assertThat(endpointErrorCause.getMessage(), is("custom exception")); + } + + @Test + public void testAbortWithExceptionAfterUpgrade() throws Exception + { + start(c -> c.addMapping("/", EchoSocket.class)); + CountDownLatch exitOnOpen = new CountDownLatch(1); + CloseTrackingEndpoint clientSocket = new CloseTrackingEndpoint() + { + @Override + public void onWebSocketConnect(Session session) + { + try + { + super.onWebSocketConnect(session); + exitOnOpen.await(); + } + catch (InterruptedException e) + { + throw new IllegalStateException(e); + } + } + }; + + // Complete the CompletableFuture with an exception the during the call to onOpened. + CompletableFuture connect = client.connect(clientSocket, WSURI.toWebsocket(server.getURI())); + assertTrue(clientSocket.openLatch.await(5, TimeUnit.SECONDS)); + assertTrue(connect.completeExceptionally(new WebSocketException("custom exception"))); + exitOnOpen.countDown(); + + // Exception from the future is correct. + ExecutionException futureError = assertThrows(ExecutionException.class, () -> connect.get(5, TimeUnit.SECONDS)); + Throwable cause = futureError.getCause(); + assertThat(cause, instanceOf(WebSocketException.class)); + assertThat(cause.getMessage(), is("custom exception")); + + // Exception from the endpoint is correct. + assertTrue(clientSocket.errorLatch.await(5, TimeUnit.SECONDS)); + Throwable endpointError = clientSocket.error.get(); + assertThat(endpointError, instanceOf(WebSocketException.class)); + assertThat(endpointError.getMessage(), is("custom exception")); + } +} \ No newline at end of file diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTimeoutTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTimeoutTest.java index 392e7eb74f2..264649db12e 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTimeoutTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTimeoutTest.java @@ -57,6 +57,7 @@ import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; +import org.opentest4j.TestAbortedException; import static org.eclipse.jetty.http.client.Transport.UNIX_SOCKET; import static org.junit.jupiter.api.Assertions.assertArrayEquals; @@ -507,7 +508,7 @@ public class HttpClientTimeoutTest extends AbstractTest // connect to them will hang the connection attempt, which is // what we want to simulate in this test. socket.connect(new InetSocketAddress(host, port), connectTimeout); - // Abort the test if we can connect. + // Fail the test if we can connect. fail("Error: Should not have been able to connect to " + host + ":" + port); } catch (SocketTimeoutException ignored) @@ -517,7 +518,7 @@ public class HttpClientTimeoutTest extends AbstractTest catch (Throwable x) { // Abort if any other exception happens. - fail(x); + throw new TestAbortedException("Not able to validate connect timeout conditions", x); } }