Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

java.lang.NullPointerException: Cannot invoke "String.startsWith(String)" because "etag" is null #11925

Closed
optyfr opened this issue Jun 15, 2024 · 11 comments · Fixed by #11930
Closed
Assignees
Labels
Bug For general bugs on Jetty side

Comments

@optyfr
Copy link

optyfr commented Jun 15, 2024

Jetty version(s)
12.0.10

Jetty Environment
ee9

Java version/vendor (use: java -version)
openjdk version "21.0.2" 2024-01-16 LTS
OpenJDK Runtime Environment Temurin-21.0.2+13 (build 21.0.2+13-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.2+13 (build 21.0.2+13-LTS, mixed mode, sharing)

OS type/version
Windows 11 and Linux Debian bookworm (12)

Description
22:46:12.082 [main] INFO org.eclipse.jetty.server.Server - jetty-12.0.8; built: 2024-03-29T19:58:19.443Z; git: ffffdcc; jvm 21.0.2+13-LTS
22:46:12.122 [main] INFO o.e.j.s.DefaultSessionIdManager - Session workerName=node0
22:46:12.132 [main] INFO o.e.j.server.handler.ContextHandler - Started oeje9n.ContextHandler$CoreContextHandler@23941fb4{ROOT,/,b=URLResource@12C8CFCA(jar:file:/D:/GIT/JRomManager/build/install/JRomManager-nogui-noarch/lib/jrm-WebClient-3.0.0.jar!/webclient/),a=AVAILABLE,h=oeje9n.ContextHandler$CoreContextHandler$CoreToNestedHandler@5d908d47{STARTED}}
22:46:12.195 [main] INFO o.e.jetty.server.AbstractConnector - Started HTTP@452e19ca{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
22:46:12.215 [main] INFO org.eclipse.jetty.server.Server - Started oejs.Server@3527942a{STARTING}[12.0.8,sto=0] @642ms
Enter 'stop' to halt:
22:46:17.755 [qtp664792509-41] WARN o.e.jetty.ee9.nested.HttpChannel - /smartgwt/sc/modules/ISC_Core.js
java.lang.NullPointerException: Cannot invoke "String.startsWith(String)" because "etag" is null
at org.eclipse.jetty.http.EtagUtils.rewriteWithSuffix(EtagUtils.java:217)
at org.eclipse.jetty.http.content.PreCompressedHttpContent.(PreCompressedHttpContent.java:46)
at org.eclipse.jetty.ee9.nested.ResourceService.doGet(ResourceService.java:316)
at org.eclipse.jetty.ee9.servlet.DefaultServlet.doGet(DefaultServlet.java:506)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:500)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:587)
at org.eclipse.jetty.ee9.servlet.ServletHolder.handle(ServletHolder.java:765)
at org.eclipse.jetty.ee9.servlet.ServletHandler.doHandle(ServletHandler.java:528)
at org.eclipse.jetty.ee9.nested.ScopedHandler.nextHandle(ScopedHandler.java:195)
at org.eclipse.jetty.ee9.nested.SessionHandler.doHandle(SessionHandler.java:476)
at org.eclipse.jetty.ee9.nested.ScopedHandler.nextHandle(ScopedHandler.java:195)
at org.eclipse.jetty.ee9.nested.ContextHandler.doHandle(ContextHandler.java:1034)
at org.eclipse.jetty.ee9.nested.ScopedHandler.nextScope(ScopedHandler.java:164)
at org.eclipse.jetty.ee9.servlet.ServletHandler.doScope(ServletHandler.java:483)
at org.eclipse.jetty.ee9.nested.ScopedHandler.nextScope(ScopedHandler.java:162)
at org.eclipse.jetty.ee9.nested.SessionHandler.doScope(SessionHandler.java:470)
at org.eclipse.jetty.ee9.nested.ScopedHandler.nextScope(ScopedHandler.java:162)
at org.eclipse.jetty.ee9.nested.ContextHandler.doScope(ContextHandler.java:955)
at org.eclipse.jetty.ee9.nested.ScopedHandler.handle(ScopedHandler.java:125)
at org.eclipse.jetty.ee9.nested.ContextHandler.handle(ContextHandler.java:1693)
at org.eclipse.jetty.ee9.nested.HttpChannel$RequestDispatchable.dispatch(HttpChannel.java:1575)
at org.eclipse.jetty.ee9.nested.HttpChannel.dispatch(HttpChannel.java:737)
at org.eclipse.jetty.ee9.nested.HttpChannel.handle(HttpChannel.java:510)
at org.eclipse.jetty.ee9.nested.ContextHandler$CoreContextHandler$CoreToNestedHandler.handle(ContextHandler.java:2727)
at org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:851)
at org.eclipse.jetty.server.handler.gzip.GzipHandler.handle(GzipHandler.java:597)
at org.eclipse.jetty.server.Server.handle(Server.java:179)
at org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:619)
at org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:411)
at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322)
at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99)
at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:478)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:441)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:293)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.produce(AdaptiveExecutionStrategy.java:195)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:979)
at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1209)
at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1164)
at java.base/java.lang.Thread.run(Thread.java:1583)

How to reproduce?
happens when serving resources with DefaultServlet and it's precompressed gzip content inside a jar archive as URLResource... My understanding is that jetty is currently unable to provide an etag from httpcontent with that use case

@joakime
Copy link
Contributor

joakime commented Jun 17, 2024

The NPE needs to be fixed, but it's caused by bad assumptions in your implementation.

Looking at your implementation and it's use of DefaultServlet you are abusing it in various ways.

https://github.com/optyfr/JRomManager/blob/master/jrmserver/src/main/java/jrm/fullserver/FullServer.java#L374-L383

The DefaultServlet on Jetty 6 thru Jetty 12 can handle servlet url-patterns of / (the default pattern), and prefix patterns (eg: /static/*) with the pathInfoOnly init-param set to True (see Jetty 10 - ServletFileServerMultipleLocations.java on jetty-examples).
That's it. Nothing else.
No support for absolute url-patterns (a pattern without a glob, eg /index.js).
No support for suffix based url-patterns (eg: *.js).

The only reason you seem to have this setup is an attempt to configure the cache-control response headers.
You should either have a custom Filter that applies it or use the Rewrite Handler on responses to control that, not via DefaultServlet configuration. (I would recommend a Filter).

@joakime
Copy link
Contributor

joakime commented Jun 17, 2024

URLResource@12C8CFCA(jar:file:/D:/GIT/JRomManager/build/install/JRomManager-nogui-noarch/lib/jrm-WebClient-3.0.0.jar!/webclient/

Why are you using a URLResource?
Knowing that URL and all of the URL stream mappings have undergone changes and deprecations back around JDK 20, and your docker image is on JDK 21, you should no longer be using URLResource.
The JDK zipfs is the preferred mechanism for accessing compressed contents with modern JDKs.

@joakime
Copy link
Contributor

joakime commented Jun 17, 2024

Looking at https://github.com/optyfr/JRomManager/blob/master/jrmserver/src/main/java/jrm/fullserver/FullServer.java#L203

You have this wrong ..

  1. you are not managing the Jetty lifecycle of those resource factories.
  2. you are not verifying that the Resource exists before using it (the return from newResource can easily be null in your codebase)

To obtain a ResourceFactory it should be bound to a component in Jetty that has a LifeCycle (such as a WebAppContext or ServletContextHandler).
To verify the return of newResouce() (any param type) or newClassLoaderResource(), use the import org.eclipse.jetty.util.resource.Resources statics.

Like this.

ResourceFactory resourceFactory = ResourceFactory.of(context);

As for the code you have as ...

	protected static Resource getClientPath(String path) throws URISyntaxException
	{
		if (path != null)
			return prf.newResource(getPath(path));
		final var p = getPath("jrt:/jrm.merged.module/webclient/");
		if (Files.exists(p))
			return prf.newResource(p);
		return urf.newResource(FullServer.class.getResource("/webclient/"));
	}

That should read like ...

    protected static Resource getClientPath(ResourceFactory resourceFactory, String path) throws IOException
    {
        Resource resource;

        if (path != null)
        {
            resource = resourceFactory.newResource(path);
            if (Resources.exists(resource))
                return resource;
            resource = resourceFactory.newClassLoaderResource(path, true);
            if (Resources.exists(resource))
                return resource;
        }

        resource = resourceFactory.newResource("jrt:/jrm.merged.module/webclient/");
        if (Resources.exists(resource))
            return resource;
        URL url = FullServer.class.getResource("/webclient/");
        if (url != null)
        {
            resource = resourceFactory.newResource(url);
            if (Resources.exists(resource))
                return resource;
        }
        throw new FileNotFoundException("Unable to find webclient path");
    }

This uses only 1 ResourceFactory, the one tied to the LifeCycle of the context.
This also uses the various ResourceFactory.newResource() and ResourceFactory.newClassLoaderResource().
It eliminates entirely the whole getPath() logic which doesn't gain you anything useful.
It also uses org.eclipse.jetty.util.resource.Resources to verify that a resource can even be created and accessed.

@optyfr
Copy link
Author

optyfr commented Jun 17, 2024

Thanks for the indications @joakime
I first replaced with filters and kept only one DefaultServlet for / but it still gave the NPE as soon I did hit a precompressed file but it still worked ok in other use cases (jrt: and file: on folder)
Then I replaced my getClientPath with the one you wrote depending on the ServletContext and using only 1 resourceFactory...
and unfortunately despite it returns a valid Resource for the jar file it now give a 404 for any request for that jar!

maybe related I noticed the warning message when launched :

o.e.j.server.handler.ContextHandler - Base Resource should not be an alias
But I don't understand why it says that it as an alias

Here is the full output when launched :

$ java -cp JRomManager.jar jrm.server.Server --debug
21:23:26.599 [main] INFO  org.eclipse.jetty.server.Server - jetty-12.0.10; built: 2024-05-30T04:40:36.563Z; git: 26106dfc84a03ddb6216062fe33b047fc332d0ce; jvm 21.0.2+13-LTS
21:23:26.611 [main] WARN  o.e.j.server.handler.ContextHandler - Base Resource should not be an alias
21:23:26.630 [main] INFO  o.e.j.s.DefaultSessionIdManager - Session workerName=node0
21:23:26.639 [main] INFO  o.e.j.server.handler.ContextHandler - Started oeje9n.ContextHandler$CoreContextHandler@5852c06f{ROOT,/,b=jar:file:/D:/GIT/JRomManager/build/install/JRomManager-nogui-noarch/lib/jrm-WebClient-3.0.0.jar!/webclient/,a=AVAILABLE,h=oeje9n.ContextHandler$CoreContextHandler$CoreToNestedHandler@4149c063{STARTED}}
21:23:26.684 [main] INFO  o.e.jetty.server.AbstractConnector - Started HTTP@bb9e6dc{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
21:23:26.698 [main] INFO  org.eclipse.jetty.server.Server - Started oejs.Server@543295b0{STARTING}[12.0.10,sto=0] @584ms
[2024-06-17 21:23:26] [CONFIG] Start server
[2024-06-17 21:23:26] [CONFIG] HTTP with port on 8080 binded to 0.0.0.0
[2024-06-17 21:23:26] [CONFIG] clientPath: jar:file:/D:/GIT/JRomManager/build/install/JRomManager-nogui-noarch/lib/jrm-WebClient-3.0.0.jar!/webclient/
[2024-06-17 21:23:26] [CONFIG] workPath: D:\git\JRomManager\build\install\JRomManager-nogui-noarch
Enter 'stop' to halt:

to get the resourceFactory I did like this :

final var context = new ServletContextHandler(ServletContextHandler.SESSIONS);
final var  resourceFactory = ResourceFactory.of(context);
context.setBaseResource(getClientPath(resourceFactory, clientPath));
context.setContextPath("/");

I also tried with ResourceFactory.root(); with no better success

I will commit what I modified, if you want to look at what I did (I only modified jrm.server.Server, not the FullServer)

@joakime
Copy link
Contributor

joakime commented Jun 17, 2024

o.e.j.server.handler.ContextHandler - Base Resource should not be an alias
But I don't understand why it says that it as an alias

The path on a Base Resource MUST be the correct path as it is stored on disk.
The fact that Windows paths are case insensitive just makes things on the web extra complicated.

I see several different cases in your output (the / vs \ differences are irrelevant on Java, only the individual path segments)

D:\git\JRomManager\build\install\JRomManager-nogui-noarch
D:/GIT/JRomManager/build/install/JRomManager-nogui-noarch/lib/jrm-WebClient-3.0.0.jar

That first directory entry git vs GIT stands out to me.

One case is an alias to the other as-stored case.

The case differences usually comes from a configuration file somewhere, where the string is being used as-is (without a real-path lookup / resolution / normalization / de-symlinking / etc) with Jetty's newResource(String).

You can fix this by always doing Path.toRealPath() before passing the value into Jetty.
But that method only applies to file system paths, URLs are a different case (esp ones that point to file: or jar: or jrt: schemes)

@joakime
Copy link
Contributor

joakime commented Jun 17, 2024

Once PR #11927 is merged, then I have a few things to do for this issue.

  • Add testcase to replicate NPE (using URLResource)
  • Fix NPE (I have a good idea where to do that)
  • Add more details to logging output of o.e.j.server.handler.ContextHandler - Base Resource should not be an alias to help troubleshoot that warning in the future.

@joakime joakime self-assigned this Jun 17, 2024
@optyfr
Copy link
Author

optyfr commented Jun 17, 2024

I managed to get it working by doing this...

URL url = FullServer.class.getResource("/webclient/");
if (url != null)
{
	if(url.toURI().getScheme().equalsIgnoreCase("jar"))
		resource = resourceFactory.newResource(FileSystems.newFileSystem(url.toURI(), Map.of()).getPath("/webclient"));
	else
		resource = resourceFactory.newResource(url);
    if (Resources.exists(resource))
        return resource;
}

That's a bit ugly but it works
what's the difference with the two URI ?
Class.getResource is returning

jar:file:/D:/GIT/JRomManager/build/install/JRomManager-nogui-noarch/lib/jrm-WebClient-3.0.0.jar!/webclient/

then FileSystems.getPath will return

jar:file:///D:/GIT/JRomManager/build/install/JRomManager-nogui-noarch/lib/jrm-WebClient-3.0.0.jar!/webclient/

jetty seems to be happier with the later because of jar:file:///

@joakime
Copy link
Contributor

joakime commented Jun 17, 2024

FullServer.class.getResource("/webclient/")

I would expect that to return the URL based on your classpath.
If your classpath entry is using incorrect case, your resulting URL here would also be using incorrect case.

jetty seems to be happier with the later because of jar:file:///

You are finding one of the oddities of java's URL class.
URL will mangle the file: scheme and use only 1 /, which causes problems with everything else that isn't Java's URL class.
This isn't unique to nested schemes jar:file: but also just a normal file: scheme too.

This isn't the only bug in the Java URL class.
Do yourself a favor and avoid Java URL as much as humanly possible.

Jetty has a utility built-in to correct that syntax btw (and it is also Windows UNC URI/URL aware)

URI uri = org.eclipse.jetty.util.URIUtil.correctURI(url.toUri())
Resource resource = resourceFactory.newResource(uri);

@optyfr
Copy link
Author

optyfr commented Jun 17, 2024

the case was correct for the class path, the "should not be an alias" message is only because of the missing //

I did found in between that correctURI utility method

So finally the NPE seems to be linked with the use of a separate URLResourceFactory, as soon as I used the more generic ResourceFactory (with a fixed uri), all started to work ok

Thank you

should I keep the issue open for reference or close it?

@joakime
Copy link
Contributor

joakime commented Jun 19, 2024

Opened PR #11930 to address the various concerns brought up here.

joakime added a commit that referenced this issue Jun 25, 2024
…esource is alias warning (#11930)

* Issue #11925 - ee9 DefaultServlet and suffix url-patterns.
* Issue #11925 - Fix NPE in EtagUtils with URLResource
* Issue #11925 - Make error message "Base Resource should not be an alias" more useful.
* Set <reuseForks> to false for problematic tests.
@joakime
Copy link
Contributor

joakime commented Jun 25, 2024

Merged PR #11930 to address the NPE in Etags with URLResource, and also make warning "Base Resource should not be an alias" more clear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug For general bugs on Jetty side
Projects
No open projects
Status: ✅ Done
2 participants