diff --git a/bolt-jakarta-jetty/pom.xml b/bolt-jakarta-jetty/pom.xml index 285f4a11f..26f745b73 100644 --- a/bolt-jakarta-jetty/pom.xml +++ b/bolt-jakarta-jetty/pom.xml @@ -17,6 +17,11 @@ A handy way to run Slack Bolt apps on Jakarta EE compatible Jetty HTTP server. https://docs.slack.dev/tools/java-slack-sdk + + 17 + 17 + + com.slack.api @@ -45,27 +50,21 @@ - jakarta.servlet - jakarta.servlet-api - ${jakarta.servlet-api.version} - compile - - - org.eclipse.jetty - jetty-servlet - ${jakarta.jetty.version} + org.eclipse.jetty.ee9 + jetty-ee9-servlet + ${jakarta.jetty12.version} compile org.eclipse.jetty jetty-server - ${jakarta.jetty.version} + ${jakarta.jetty12.version} compile - org.eclipse.jetty - jetty-webapp - ${jakarta.jetty.version} + org.eclipse.jetty.ee9 + jetty-ee9-webapp + ${jakarta.jetty12.version} compile diff --git a/bolt-jakarta-jetty/src/main/java/com/slack/api/bolt/jakarta_jetty/SlackAppServer.java b/bolt-jakarta-jetty/src/main/java/com/slack/api/bolt/jakarta_jetty/SlackAppServer.java index c36548340..2c7f0afce 100644 --- a/bolt-jakarta-jetty/src/main/java/com/slack/api/bolt/jakarta_jetty/SlackAppServer.java +++ b/bolt-jakarta-jetty/src/main/java/com/slack/api/bolt/jakarta_jetty/SlackAppServer.java @@ -7,17 +7,20 @@ import com.slack.api.bolt.jakarta_servlet.SlackOAuthAppServlet; import com.slack.api.bolt.jakarta_servlet.WebEndpointServlet; import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.ee9.servlet.ServletContextHandler; +import org.eclipse.jetty.ee9.servlet.ServletHolder; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ErrorHandler; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; +import java.io.PrintWriter; import java.io.Writer; +import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; @@ -36,17 +39,69 @@ public class SlackAppServer { // This is intentionally mutable to allow developers to register their own one private ErrorHandler errorHandler = new ErrorHandler() { @Override + protected void writeErrorHtml( + Request request, + Writer writer, + Charset charset, + int code, + String message, + Throwable cause) throws IOException { + if (writeMinimalError(writer, code)) { + return; + } + super.writeErrorHtml(request, writer, charset, code, message, cause); + } + + @Override + protected void writeErrorJson( + Request request, PrintWriter writer, int code, String message, Throwable cause) { + if (writeMinimalError(writer, code)) { + return; + } + super.writeErrorJson(request, writer, code, message, cause); + } + + @Override + protected void writeErrorPlain( + Request request, PrintWriter writer, int code, String message, Throwable cause) { + if (writeMinimalError(writer, code)) { + return; + } + super.writeErrorPlain(request, writer, code, message, cause); + } + }; + + private final org.eclipse.jetty.ee9.nested.ErrorHandler servletErrorHandler = + new org.eclipse.jetty.ee9.nested.ErrorHandler() { + @Override protected void writeErrorPage( HttpServletRequest request, Writer writer, int code, String message, boolean showStacks) throws IOException { - if (localDebug) { - super.writeErrorPage(request, writer, code, message, showStacks); - } else { - writer.write("{\"status\":\"" + code + "\"}"); + if (writeMinimalError(writer, code)) { + return; + } + super.writeErrorPage(request, writer, code, message, showStacks); + } + + @Override + protected void writeErrorJson( + HttpServletRequest request, PrintWriter writer, int code, String message) { + if (writeMinimalError(writer, code)) { + return; } + super.writeErrorJson(request, writer, code, message); + } + + @Override + protected void writeErrorPlain( + HttpServletRequest request, PrintWriter writer, int code, String message) { + if (writeMinimalError(writer, code)) { + return; + } + super.writeErrorPlain(request, writer, code, message); } }; @@ -119,6 +174,7 @@ public SlackAppServer(Map pathToApp, int port) { } } pathToApp.putAll(addedOnes); + handler.setErrorHandler(servletErrorHandler); server.setHandler(handler); server.setErrorHandler(errorHandler); } @@ -152,6 +208,22 @@ public void setErrorHandler(ErrorHandler errorHandler) { // internal methods // ---------------------------------------------------- + private boolean writeMinimalError(Writer writer, int code) throws IOException { + if (localDebug) { + return false; + } + writer.write("{\"status\":\"" + code + "\"}"); + return true; + } + + private boolean writeMinimalError(PrintWriter writer, int code) { + if (localDebug) { + return false; + } + writer.write("{\"status\":\"" + code + "\"}"); + return true; + } + private static Map toApps(App app, String path) { Map apps = new HashMap<>(); apps.put(path, app); diff --git a/bolt-jakarta-jetty/src/test/java/test_locally/jakarta_jetty/SlackAppServerTest.java b/bolt-jakarta-jetty/src/test/java/test_locally/jakarta_jetty/SlackAppServerTest.java new file mode 100644 index 000000000..06c8b6145 --- /dev/null +++ b/bolt-jakarta-jetty/src/test/java/test_locally/jakarta_jetty/SlackAppServerTest.java @@ -0,0 +1,121 @@ +package test_locally.jakarta_jetty; + +import com.slack.api.bolt.App; +import com.slack.api.bolt.AppConfig; +import com.slack.api.bolt.jakarta_jetty.SlackAppServer; +import org.junit.Test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.ServerSocket; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assume.assumeThat; + +public class SlackAppServerTest { + + @Test + public void startsAndServesMinimalErrorResponseForEverySupportedAcceptType() throws Exception { + assumeThat(System.getenv("SLACK_APP_LOCAL_DEBUG"), is(nullValue())); + + int port = findAvailablePort(); + App app = new App(new AppConfig(), Collections.emptyList()); + SlackAppServer server = new SlackAppServer(app, port); + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future serverFuture = executor.submit(() -> { + server.start(); + return null; + }); + + try { + String[][] acceptTypes = { + {null, "text/html"}, + {"text/html", "text/html"}, + {"text/*", "text/html"}, + {"*/*", "text/html"}, + {"application/json", "application/json"}, + {"text/json", "text/json"}, + {"text/plain", "text/plain"} + }; + for (String[] acceptType : acceptTypes) { + HttpURLConnection connection = awaitResponse(serverFuture, port, acceptType[0]); + try { + assertMinimalErrorResponse(connection, acceptType[0], acceptType[1]); + } finally { + connection.disconnect(); + } + } + } finally { + try { + server.stop(); + serverFuture.get(10, TimeUnit.SECONDS); + } finally { + executor.shutdownNow(); + } + } + } + + private static void assertMinimalErrorResponse( + HttpURLConnection connection, String acceptType, String expectedContentType) throws IOException { + String requestDescription = acceptType == null ? "with default Accept" : "with Accept: " + acceptType; + assertThat("status " + requestDescription, connection.getResponseCode(), is(equalTo(404))); + assertThat("Server header " + requestDescription, connection.getHeaderField("Server"), is(nullValue())); + assertThat("Content-Type " + requestDescription, + connection.getContentType(), startsWith(expectedContentType)); + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + connection.getErrorStream(), StandardCharsets.UTF_8))) { + assertThat("body " + requestDescription, + reader.lines().collect(Collectors.joining("\n")), + is(equalTo("{\"status\":\"404\"}"))); + } + } + + private static int findAvailablePort() throws IOException { + try (ServerSocket socket = new ServerSocket(0)) { + return socket.getLocalPort(); + } + } + + private static HttpURLConnection awaitResponse(Future serverFuture, int port, String acceptType) + throws Exception { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + IOException lastFailure = null; + while (System.nanoTime() < deadline) { + if (serverFuture.isDone()) { + serverFuture.get(); + throw new AssertionError("SlackAppServer stopped before accepting requests"); + } + + HttpURLConnection connection = (HttpURLConnection) new URL( + "http://127.0.0.1:" + port + "/missing").openConnection(); + if (acceptType != null) { + connection.setRequestProperty("Accept", acceptType); + } + connection.setConnectTimeout(200); + connection.setReadTimeout(200); + try { + connection.getResponseCode(); + return connection; + } catch (IOException e) { + lastFailure = e; + connection.disconnect(); + Thread.sleep(10); + } + } + throw new AssertionError("SlackAppServer did not accept requests within 10 seconds", lastFailure); + } +} diff --git a/pom.xml b/pom.xml index f520ed32c..8b4a7ccaf 100644 --- a/pom.xml +++ b/pom.xml @@ -69,8 +69,10 @@ target/generated-sources-for-javadocs - + [11.0,11.1) + + [12.1,12.2) [4.0,5.0) [2.29.24,3) 4.13.2 diff --git a/scripts/run_no_prep_tests.sh b/scripts/run_no_prep_tests.sh index 2b5a9ddf6..dd4c1e145 100755 --- a/scripts/run_no_prep_tests.sh +++ b/scripts/run_no_prep_tests.sh @@ -33,6 +33,7 @@ then elif [[ "${is_jdk_14}" != "" ]]; then ./mvnw \ + -pl !bolt-jakarta-jetty \ -pl !bolt-micronaut \ $MVN_PHASES \ '-Dtest=test_locally.**.*Test' -Dsurefire.failIfNoSpecifiedTests=false ${CI_ARGS} \