Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.eclipse.jetty.ee8.security.RoleInfo;
import org.eclipse.jetty.ee8.security.SecurityHandler;
import org.eclipse.jetty.ee8.security.UserDataConstraint;
import org.eclipse.jetty.ee8.servlet.ServletHandler;
import org.eclipse.jetty.ee8.webapp.WebAppContext;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
Expand Down Expand Up @@ -70,6 +71,8 @@ public AppEngineWebAppContext(File appDir, String serverInfo) {

this.serverInfo = serverInfo;

setThrowUnavailableOnStartupException(true);

// Configure the Jetty SecurityHandler to understand our method of
// authentication (via the UserService).
AppEngineAuthentication.configureSecurityHandler(
Expand All @@ -78,6 +81,57 @@ public AppEngineWebAppContext(File appDir, String serverInfo) {
setMaxFormContentSize(MAX_RESPONSE_SIZE);
}

/**
* Configures the {@link ServletHandler} to disallow starting with unavailable servlets or
* filters.
*
* <p>Setting {@code setStartWithUnavailable(false)} ensures that any servlet or filter
* initialization failure throws an exception up the startup lifecycle chain rather than silently
* marking the handler component as unavailable.
*/
@Override
protected ServletHandler newServletHandler() {
ServletHandler handler = new ServletHandler();
handler.setStartWithUnavailable(false);
return handler;
}

/**
* Overrides {@code doStart} to ensure that any initialization errors (such as a missing servlet
* class defined in {@code web.xml}) are reported as fatal startup exceptions.
*
* <p>By default, Jetty may catch {@link ClassNotFoundException} or {@link UnavailableException}
* during {@link ServletHandler#initialize()} and mark the individual {@code ServletHolder} as
* unavailable without failing context startup. We inspect the context and all registered
* servlets; if any unavailable exception was caught during startup, we rethrow it immediately so
* application deployment terminates rather than serving HTTP 503 errors at runtime.
*
* @throws Exception if the context or any of its servlets fail to initialize.
* @see <a href="https://github.com/GoogleCloudPlatform/appengine-java-standard/issues/103">Issue #103</a>
*/
@Override
protected void doStart() throws Exception {
super.doStart();
Throwable t = getUnavailableException();
if (t != null) {
if (t instanceof Exception) {
throw (Exception) t;
}
if (t instanceof Error) {
throw (Error) t;
}
throw new IllegalStateException("Context initialization failed", t);
}
ServletHandler servletHandler = getServletHandler();
if (servletHandler != null && servletHandler.getServlets() != null) {
for (var holder : servletHandler.getServlets()) {
if (holder.getUnavailableException() != null) {
throw holder.getUnavailableException();
}
}
}
}

@Override
public APIContext getServletContext() {
// TODO: Override the default HttpServletContext implementation (for logging)?.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.runtime.jetty.EE11AppEngineAuthentication;
import java.io.File;
import org.eclipse.jetty.ee11.servlet.ServletHandler;
import org.eclipse.jetty.ee11.servlet.security.ConstraintSecurityHandler;
import org.eclipse.jetty.ee11.webapp.WebAppContext;
import org.eclipse.jetty.security.Constraint;
Expand Down Expand Up @@ -66,13 +67,66 @@ public AppEngineWebAppContext(File appDir, String serverInfo) {

this.serverInfo = serverInfo;

setThrowUnavailableOnStartupException(true);

// Configure the Jetty SecurityHandler to understand our method of
// authentication (via the UserService).
setSecurityHandler(EE11AppEngineAuthentication.newSecurityHandler());

setMaxFormContentSize(MAX_RESPONSE_SIZE);
}

/**
* Configures the {@link ServletHandler} to disallow starting with unavailable servlets or
* filters.
*
* <p>Setting {@code setStartWithUnavailable(false)} ensures that any servlet or filter
* initialization failure throws an exception up the startup lifecycle chain rather than silently
* marking the handler component as unavailable.
*/
@Override
protected ServletHandler newServletHandler() {
ServletHandler handler = new ServletHandler();
handler.setStartWithUnavailable(false);
return handler;
}

/**
* Overrides {@code doStart} to ensure that any initialization errors (such as a missing servlet
* class defined in {@code web.xml}) are reported as fatal startup exceptions.
*
* <p>By default, Jetty may catch {@link ClassNotFoundException} or {@link UnavailableException}
* during {@link ServletHandler#initialize()} and mark the individual {@code ServletHolder} as
* unavailable without failing context startup. We inspect the context and all registered
* servlets; if any unavailable exception was caught during startup, we rethrow it immediately so
* application deployment terminates rather than serving HTTP 503 errors at runtime.
*
* @throws Exception if the context or any of its servlets fail to initialize.
* @see <a href="https://github.com/GoogleCloudPlatform/appengine-java-standard/issues/103">Issue #103</a>
*/
@Override
protected void doStart() throws Exception {
super.doStart();
Throwable t = getUnavailableException();
if (t != null) {
if (t instanceof Exception) {
throw (Exception) t;
}
if (t instanceof Error) {
throw (Error) t;
}
throw new IllegalStateException("Context initialization failed", t);
}
ServletHandler servletHandler = getServletHandler();
if (servletHandler != null && servletHandler.getServlets() != null) {
for (var holder : servletHandler.getServlets()) {
if (holder.getUnavailableException() != null) {
throw holder.getUnavailableException();
}
}
}
}

@Override
public ServletScopedContext getContext() {
// TODO: Override the default HttpServletContext implementation (for logging)?.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,40 @@ public boolean removeEventListener(EventListener listener) {
return false;
}

/**
* Overrides {@code doStart} to ensure that any initialization errors (such as a missing servlet
* class defined in {@code web.xml}) are reported as fatal startup exceptions.
*
* <p>By default, Jetty may catch {@link ClassNotFoundException} or {@link UnavailableException}
* during {@link ServletHandler#initialize()} and mark the individual {@code ServletHolder} as
* unavailable without failing context startup. We inspect the context and all registered
* servlets; if any unavailable exception was caught during startup, we rethrow it immediately so
* application deployment terminates rather than serving HTTP 503 errors at runtime.
*
* @throws Exception if the context or any of its servlets fail to initialize.
* @see <a href="https://github.com/GoogleCloudPlatform/appengine-java-standard/issues/103">Issue #103</a>
*/
@Override
public void doStart() throws Exception {
super.doStart();
Throwable t = getUnavailableException();
if (t != null) {
if (t instanceof Exception) {
throw (Exception) t;
}
if (t instanceof Error) {
throw (Error) t;
}
throw new IllegalStateException("Context initialization failed", t);
}
ServletHandler servletHandler = getServletHandler();
if (servletHandler != null && servletHandler.getServlets() != null) {
for (var holder : servletHandler.getServlets()) {
if (holder.getUnavailableException() != null) {
throw holder.getUnavailableException();
}
}
}
addEventListener(new TransactionCleanupListener(getClassLoader()));
}

Expand Down Expand Up @@ -275,10 +306,19 @@ public boolean handle(Request request, Response response, Callback callback) thr
}
}

/**
* Configures the {@link ServletHandler} to disallow starting with unavailable servlets or
* filters.
*
* <p>Setting {@code setStartWithUnavailable(false)} ensures that any servlet or filter
* initialization failure throws an exception up the startup lifecycle chain rather than silently
* marking the handler component as unavailable.
*/
@Override
protected ServletHandler newServletHandler() {
ServletHandler handler = new ServletHandler();
handler.setAllowDuplicateMappings(true);
handler.setStartWithUnavailable(false);
if (AppEngineConstants.isLegacyMode()) {
handler.setDecodeAmbiguousURIs(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,40 @@ public boolean removeEventListener(EventListener listener) {
return false;
}

/**
* Overrides {@code doStart} to ensure that any initialization errors (such as a missing servlet
* class defined in {@code web.xml}) are reported as fatal startup exceptions.
*
* <p>By default, Jetty may catch {@link ClassNotFoundException} or {@link UnavailableException}
* during {@link ServletHandler#initialize()} and mark the individual {@code ServletHolder} as
* unavailable without failing context startup. We inspect the context and all registered
* servlets; if any unavailable exception was caught during startup, we rethrow it immediately so
* application deployment terminates rather than serving HTTP 503 errors at runtime.
*
* @throws Exception if the context or any of its servlets fail to initialize.
* @see <a href="https://github.com/GoogleCloudPlatform/appengine-java-standard/issues/103">Issue #103</a>
*/
@Override
public void doStart() throws Exception {
super.doStart();
Throwable t = getUnavailableException();
if (t != null) {
if (t instanceof Exception) {
throw (Exception) t;
}
if (t instanceof Error) {
throw (Error) t;
}
throw new IllegalStateException("Context initialization failed", t);
}
ServletHandler servletHandler = getServletHandler();
if (servletHandler != null && servletHandler.getServlets() != null) {
for (var holder : servletHandler.getServlets()) {
if (holder.getUnavailableException() != null) {
throw holder.getUnavailableException();
}
}
}
addEventListener(new TransactionCleanupListener(getClassLoader()));
}

Expand Down Expand Up @@ -312,10 +343,19 @@ public void doHandle(
}
}

/**
* Configures the {@link ServletHandler} to disallow starting with unavailable servlets or
* filters.
*
* <p>Setting {@code setStartWithUnavailable(false)} ensures that any servlet or filter
* initialization failure throws an exception up the startup lifecycle chain rather than silently
* marking the handler component as unavailable.
*/
@Override
protected ServletHandler newServletHandler() {
ServletHandler handler = new ServletHandler();
handler.setAllowDuplicateMappings(true);
handler.setStartWithUnavailable(false);
return handler;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.google.appengine.tools.development.resource.ResourceExtractor;
Expand All @@ -27,6 +28,7 @@
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.jar.JarEntry;
Expand Down Expand Up @@ -133,4 +135,55 @@ public void doesntExtractWar() throws Exception {
assertEquals(files.length, 0);
}
}

@Test
public void missingServletClassThrowsOnStartupEe8() throws Exception {
File appDir = temporaryFolder.newFolder("missingservletapp-ee8");
File webInf = new File(appDir, "WEB-INF");
webInf.mkdirs();
File webXml = new File(webInf, "web.xml");
Files.writeString(
webXml.toPath(),
"<web-app xmlns=\"http://xmlns.jcp.org/xml/ns/javaee\" version=\"4.0\">\n"
+ " <servlet>\n"
+ " <servlet-name>MissingServlet</servlet-name>\n"
+ " <servlet-class>com.example.nonexistent.MissingServlet</servlet-class>\n"
+ " </servlet>\n"
+ " <servlet-mapping>\n"
+ " <servlet-name>MissingServlet</servlet-name>\n"
+ " <url-pattern>/missing</url-pattern>\n"
+ " </servlet-mapping>\n"
+ "</web-app>\n");

AppEngineWebAppContext context = new AppEngineWebAppContext(appDir, "test server");
context.setTempDirectory(new File(appDir, "tmp"));
context.setServer(new org.eclipse.jetty.server.Server());
assertThrows(Exception.class, context::doStart);
}

@Test
public void missingServletClassThrowsOnStartupEe10() throws Exception {
File appDir = temporaryFolder.newFolder("missingservletapp-ee10");
File webInf = new File(appDir, "WEB-INF");
webInf.mkdirs();
File webXml = new File(webInf, "web.xml");
Files.writeString(
webXml.toPath(),
"<web-app xmlns=\"https://jakarta.ee/xml/ns/jakartaee\" version=\"6.0\">\n"
+ " <servlet>\n"
+ " <servlet-name>MissingServlet</servlet-name>\n"
+ " <servlet-class>com.example.nonexistent.MissingServlet</servlet-class>\n"
+ " </servlet>\n"
+ " <servlet-mapping>\n"
+ " <servlet-name>MissingServlet</servlet-name>\n"
+ " <url-pattern>/missing</url-pattern>\n"
+ " </servlet-mapping>\n"
+ "</web-app>\n");

com.google.apphosting.runtime.jetty.ee10.AppEngineWebAppContext context =
new com.google.apphosting.runtime.jetty.ee10.AppEngineWebAppContext(appDir, "test server", true);
context.setTempDirectory(new File(appDir, "tmp"));
context.setServer(new org.eclipse.jetty.server.Server());
assertThrows(Exception.class, context::doStart);
}
}
Loading
Loading