CrossContextGetServletContextBug.java
Jetty version: 12.1.12
Java version: 21
OS: macOS (not OS-specific)
Description
When a servlet in context A performs a cross-context RequestDispatcher.include() into a servlet in context B, the included servlet's own getServletContext() (bound at init()) correctly reports context B. But ServletRequest.getServletContext() on the request object passed to that servlet still reports context A — the original, dispatching context — not context B.
Per the Servlet spec, ServletRequest.getServletContext() should return "the servlet context to which this ServletRequest was last dispatched", which for code running inside the include() target ought to be context B.
This matters for any code that looks up its WebApplicationContext/config/state via request.getServletContext() rather than its own inherited getServletContext() — it silently gets the wrong context's data instead of an error, which made this tricky to notice.
Minimal reproduction
Self-contained, only needs jetty-server, jetty-ee11-servlet, jetty-security, jetty-session, jetty-http, jetty-io, jetty-util, jakarta.servlet-api:6.1.0, slf4j-api on the classpath:
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
/**
* EntryServlet (context "/a") performs a cross-context RequestDispatcher.include()
* into TargetServlet (context "/b"), using a directly-held ServletContext reference
* obtained from ServletContextHandler.getServletContext() (the usual workaround for
* ServletContext.getContext(String) being restricted).
*
* Expected: request.getServletContext() inside TargetServlet reports "/b".
* Actual: it reports "/a" -- the ORIGINAL context.
*/
public class CrossContextGetServletContextBug {
static ServletContext contextB;
public static class EntryServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
contextB.getRequestDispatcher("/target").include(req, resp);
}
}
public static class TargetServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String viaOwnContext = getServletContext().getContextPath(); // correct: set at init() time
String viaRequest = req.getServletContext().getContextPath(); // spec: should be "/b"
resp.setContentType("text/plain");
resp.getWriter().println("getServletContext().getContextPath() = " + viaOwnContext);
resp.getWriter().println("request.getServletContext().getContextPath() = " + viaRequest);
}
}
public static void main(String[] args) throws Exception {
Server server = new Server(0);
ServletContextHandler ctxA = new ServletContextHandler("/a");
ctxA.addServlet(EntryServlet.class, "/entry");
ServletContextHandler ctxB = new ServletContextHandler("/b");
ctxB.addServlet(TargetServlet.class, "/target");
contextB = ctxB.getServletContext();
ContextHandlerCollection contexts = new ContextHandlerCollection();
contexts.addHandler(ctxA);
contexts.addHandler(ctxB);
server.setHandler(contexts);
server.start();
int port = ((ServerConnector) server.getConnectors()[0]).getLocalPort();
String body = HttpClient.newHttpClient()
.send(HttpRequest.newBuilder(URI.create("http://localhost:" + port + "/a/entry")).GET().build(),
HttpResponse.BodyHandlers.ofString())
.body();
System.out.println(body);
System.out.println(body.contains("request.getServletContext().getContextPath() = /b")
? "PASS: request.getServletContext() correctly reflects the include target"
: "FAIL (bug reproduced): request.getServletContext() did not return the include target's context");
server.stop();
}
}
Expected output:
getServletContext().getContextPath() = /b
request.getServletContext().getContextPath() = /b
Actual output (as run against 12.1.12):
getServletContext().getContextPath() = /b
request.getServletContext().getContextPath() = /a
FAIL (bug reproduced): request.getServletContext() did not return the include target's context
CrossContextGetServletContextBug.java
Jetty version: 12.1.12
Java version: 21
OS: macOS (not OS-specific)
Description
When a servlet in context A performs a cross-context
RequestDispatcher.include()into a servlet in context B, the included servlet's owngetServletContext()(bound atinit()) correctly reports context B. ButServletRequest.getServletContext()on the request object passed to that servlet still reports context A — the original, dispatching context — not context B.Per the Servlet spec,
ServletRequest.getServletContext()should return "the servlet context to which this ServletRequest was last dispatched", which for code running inside the include() target ought to be context B.This matters for any code that looks up its
WebApplicationContext/config/state viarequest.getServletContext()rather than its own inheritedgetServletContext()— it silently gets the wrong context's data instead of an error, which made this tricky to notice.Minimal reproduction
Self-contained, only needs
jetty-server,jetty-ee11-servlet,jetty-security,jetty-session,jetty-http,jetty-io,jetty-util,jakarta.servlet-api:6.1.0,slf4j-apion the classpath:Expected output:
Actual output (as run against 12.1.12):