jetty.project icon indicating copy to clipboard operation
jetty.project copied to clipboard

PathMappingsHandler does not start ResourceHandler properly

Open hydrozoa-yt opened this issue 1 year ago • 4 comments

Jetty version(s) 12.0.6

Jetty Environment core

Java version/vendor (use: java -version) OpenJDK 64-Bit Server VM (build 20.0.1+9-29, mixed mode, sharing)

OS type/version Windows 11

Description I'm suspecting I've found a bug with ResourceHandler after upgrading to jetty 12. I can't get any files served unless I nest the ResourceHandler in a ContextHandler, the latter having set the needed base resource. And even then there's behavior I don't understand where some configs will yield only 404s. Ideally, ResourceHandler should work out of the box withouit being nested in a ContextHandler, and behaving in a predictable way. I've made this simplest example of the issue, based on the example in the programming guide, and it can only serve 404s. I've verified that the Path and underlying Resource are valid.

Here's the error when requesting something from the ResourceHandler: Screenshot 2024-02-15 124435

Here's the verification that this is not a problem with an invalid Path or null Resource: Screenshot 2024-02-15 124258

How to reproduce?

public class Main implements Runnable {

    public final int PORT = 8443;
    private Server jettyServer;

    public static void main(String[] args) {
        new Main().run();
    }

    @Override
    public void run() {
        jettyServer = new Server();

        ServerConnector connector = new ServerConnector(jettyServer);
        connector.setPort(PORT);

        jettyServer.addConnector(connector);

        PathMappingsHandler mapHandler = new PathMappingsHandler();
        jettyServer.setHandler(mapHandler);

        ResourceHandler resourceHandler = new ResourceHandler();
        Path baseDir = Path.of("resources/public/");
        Resource baseResource = ResourceFactory.of(resourceHandler).newResource(baseDir);
        if (!Resources.isReadableDirectory(baseResource)) {
            throw new RuntimeException("Resource is not a readable directory");
        }
        resourceHandler.setBaseResource(baseResource);
        resourceHandler.setDirAllowed(true);
        resourceHandler.setUseFileMapping(true);
        resourceHandler.setServer(jettyServer);
        mapHandler.addMapping(PathSpec.from("/files/*"), resourceHandler);

        jettyServer.setDumpAfterStart(true);

        try {
            jettyServer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

hydrozoa-yt avatar Feb 15 '24 12:02 hydrozoa-yt

The output of your debug is useful!

It is not an alias, if it was, then things get complicated. But the resulting resolved resource might be.

Can you make this change and try again?

        ContextHandlerCollection handlers = new ContextHandlerCollection();
        jettyServer.setHandler(handlers);

        ResourceHandler resourceHandler = new ResourceHandler();
        Path baseDir = Path.of("resources/public/");
        Resource baseResource = ResourceFactory.of(resourceHandler).newResource(baseDir);
        if (!Resources.isReadableDirectory(baseResource)) {
            throw new RuntimeException("Resource is not a readable directory");
        }
        resourceHandler.setBaseResource(baseResource);
        resourceHandler.setDirAllowed(true);
        resourceHandler.setUseFileMapping(true);
        resourceHandler.setServer(jettyServer);

        ContextHandler contextHandler = new ContextHandler("/files");
        contextHandler.addAliasCheck((p, r) -> true); // this is the part that i'm having you test
        contextHandler.setHandler(resourceHandler);
        handlers.addHandler(contextHandler);

Also, does this example work for you?

  • https://github.com/jetty/jetty-examples/tree/12.0.x/embedded/file-server

joakime avatar Feb 15 '24 13:02 joakime

One bug has been discovered from your report. It has been addressed in PR #11412

This is unlikely to fix your specific issue though, just an ancillary bug that needs to be fixed.

joakime avatar Feb 15 '24 20:02 joakime

I understand this better now.

Before I get into the why, let me start by giving you a working example.

See: https://github.com/jetty/jetty-examples/tree/12.0.x/embedded/path-mapping-handler

First, due to the bug in #11412 you'll need to work around that by simply moving your setHandler() call.

PathMappingsHandler mapHandler = new PathMappingsHandler();

ResourceHandler resourceHandler = new ResourceHandler();
Path baseDir = Path.of("resources/public/");
Resource baseResource = ResourceFactory.of(resourceHandler).newResource(baseDir);
if (!Resources.isReadableDirectory(baseResource)) {
    throw new RuntimeException("Resource is not a readable directory");
}
resourceHandler.setBaseResource(baseResource);
resourceHandler.setDirAllowed(true);
resourceHandler.setUseFileMapping(true);
// resourceHandler.setServer(jettyServer); // eliminate this line
mapHandler.addMapping(PathSpec.from("/files/*"), resourceHandler);

jettyServer.setHandler(mapHandler); // move this line to here

jettyServer.setDumpAfterStart(true);
jettyServer.start();

That is enough to workaround the bug in PR #11412.

Now for the other part, you have a mapping of /files/* to ResourceHandler.

The ResourceHandler needs to know what parts of the request path are relevant to use against the Base Resource. This is traditionally done with a simple Context-Path approach (using the ContextHandler wrapper).

Take this example...

Your configuration:

  • You have files you want to serve from a filesystem path of /home/user/webroot/
  • You have that directory mapped against the path-spec of /files/*

An incoming request:

  • The request arrives with the path /files/css/main.css
  • The ResourceHandler mapped at /files/* is called.
  • The ResourceHandler has a base resource of /home/user/webroot/
  • The resulting file that is looked for is /home/user/webroot/files/css/main.css (effectively the concatenation of <base-dir> + <request-path>)

Now when we add a Context to this we can have the Context determine what is "the path in context".

So a ContextHandler("/files", filesResourceHandler) would strip the /files portion from the request path and then it reaches the ResourceHandler for file /home/user/webroot/css/main.css

But PathMappingsHandler does not have a context or context-path. This might seem straight forward for the PathMappingsHandler to just implement, however things get confusing when you start mapping by suffixes (or using regex, or using uri-templates).

What if you had a mapping using a suffix match?

pathMappingsHandler.addMapping(PathSpec.from("*.png"), resourceHandler);

How do you determine the actual file you want to serve from the ResourceHandler from that? (the request path could be /deep/foo/image.png or /image.png or /alternate/pix/logo.png, etc, but you want to always serve those request from your filesystem /home/user/webimages/ and without the directory names at all) So you can see that for non-root ResourceHandler mappings you'll need to determine how to map the request path to the resource handler.

The example linked above shows one way to do that within the PathMappingServer.java code.

joakime avatar Feb 15 '24 21:02 joakime

Thank you for the swift and excellent help! Thanks to your explanation and example I can now better understand why it behaves as it does.

I've changed the code, and it works exactly as expected without any messy ContextHandlers. I'll leave it below for future browsers.

Also, I'm very happy about the PathMappingsHandler setServer fix, it was getting cumbersome adding it manually to all handlers.

public class Main implements Runnable {

    public final int PORT = 8443;
    private Server jettyServer;

    public static void main(String[] args) {
        new Main().run();
    }

    @Override
    public void run() {
        jettyServer = new Server();

        ServerConnector connector = new ServerConnector(jettyServer);
        connector.setPort(PORT);

        jettyServer.addConnector(connector);

        PathMappingsHandler pathMappingsHandler = new PathMappingsHandler();

        ResourceFactory resourceFactory = ResourceFactory.of(jettyServer);

        Resource baseResource = resourceFactory.newResource(Path.of("resources/public/"));
        if (!Resources.isReadableDirectory(baseResource)) {
            throw new RuntimeException("Resource is not a readable directory");
        }
        ResourceHandler resourceHandler = new ResourceHandler();
        resourceHandler.setBaseResource(baseResource);
        resourceHandler.setDirAllowed(true);
        resourceHandler.setUseFileMapping(true);

        StripContextPath stripResHandler = new StripContextPath("/files", resourceHandler);
        pathMappingsHandler.addMapping(PathSpec.from("/files/*"), stripResHandler);

        jettyServer.setHandler(pathMappingsHandler);

        try {
            jettyServer.start();
            jettyServer.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static class StripContextPath extends PathNameWrapper {
        public StripContextPath(String contextPath, Handler handler) {
            super((path) -> {
                if (path.startsWith(contextPath))
                    return path.substring(contextPath.length());
                return path;
            }, handler);
        }
    }

    private static class PathNameWrapper extends Handler.Wrapper {
        private final Function<String, String> nameFunction;

        public PathNameWrapper(Function<String, String> nameFunction, Handler handler) {
            super(handler);
            this.nameFunction = nameFunction;
        }

        @Override
        public boolean handle(Request request, Response response, Callback callback) throws Exception {
            String originalPath = request.getHttpURI().getPath();

            String newPath = nameFunction.apply(originalPath);

            HttpURI newURI = HttpURI.build(request.getHttpURI()).path(newPath);

            Request wrappedRequest = new Request.Wrapper(request) {
                @Override
                public HttpURI getHttpURI() {
                    return newURI;
                }
            };

            return super.handle(wrappedRequest, response, callback);
        }
    }
}

hydrozoa-yt avatar Feb 16 '24 11:02 hydrozoa-yt