jetty.project
jetty.project copied to clipboard
PathMappingsHandler does not start ResourceHandler properly
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:
Here's the verification that this is not a problem with an invalid Path or null Resource:
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();
}
}
}
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
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.
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.
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);
}
}
}