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} \