Skip to content

ee11 servlet: ServletRequest.getServletContext() returns the source context, not the target, after a cross-context RequestDispatcher.include() #15733

Description

@peder-sperstad-eb

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions