Home > Enterprise >  Jetty 11.0.11 - 404 on html file in \src\main\webapp\static - maven embedded fat jar
Jetty 11.0.11 - 404 on html file in \src\main\webapp\static - maven embedded fat jar

Time:09-06

I have a file in my Netbeans 14 Jetty 11.0.11 project at \src\main\webapp\static\index.html that I want to serve to web-browsers using Jetty.

Here is my pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>verishare</groupId>
    <artifactId>verdi</artifactId>
    <version>12-JDK17</version>
    <packaging>jar</packaging>

    <name>verdi</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jettyVersion>11.0.11</jettyVersion>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.21</version>
        </dependency>                                       
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <!--<version>2.11.0</version>-->
            <version>2.17.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <!--<version>2.11.0</version>-->
            <version>2.17.1</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.eclipse.jetty.websocket</groupId>
            <artifactId>websocket-jetty-server</artifactId>
            <version>${jettyVersion}</version>
        </dependency>        
        <dependency>
            <groupId>org.eclipse.jetty.websocket</groupId>
            <artifactId>websocket-jetty-client</artifactId>
            <version>${jettyVersion}</version>
        </dependency>


        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>${jettyVersion}</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-servlet</artifactId>
            <version>${jettyVersion}</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-webapp</artifactId>
            <version>${jettyVersion}</version>
        </dependency>        
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-annotations</artifactId>
            <version>${jettyVersion}</version>
        </dependency>        
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>apache-jsp</artifactId>
            <version>${jettyVersion}</version>
        </dependency>
        
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>apache-jstl</artifactId>
            <version>11.0.0</version>        
        </dependency>
        
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.2.4</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.json</artifactId>
            <version>1.0.4</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-dbcp2</artifactId>
            <!--<version>2.0.1</version>-->
            <version>2.5.0</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>9.1-901-1.jdbc4</version>
        </dependency>
        <!--
        <dependency>
            <groupId>org.xerial</groupId>
            <artifactId>sqlite-jdbc</artifactId>
            <version>3.8.6</version>
        </dependency>
        -->
        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>mssql-jdbc</artifactId>
            <version>6.4.0.jre8</version>
            <!--<scope>test</scope>-->
        </dependency>
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <!--<version>1.1.8</version>-->
            <version>2.7.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.1</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.core</groupId>
            <artifactId>jersey-server</artifactId>
            <version>2.22.1</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-servlet-core</artifactId>
            <version>2.22.1</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-multipart</artifactId>
            <version>2.22.1</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-jackson</artifactId>
            <version>2.22.1</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-jetty-http</artifactId>
            <version>2.22.1</version>
        </dependency>
        <dependency>
            <groupId>org.reflections</groupId>
            <artifactId>reflections</artifactId>
            <version>0.9.10</version>
        </dependency>
        <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jsch</artifactId>
            <version>0.1.53</version>
        </dependency>
        <dependency>
            <groupId>ie.corballis</groupId>
            <artifactId>sox-java</artifactId>
            <version>1.0.1</version>
        </dependency>
        <dependency>
            <groupId>com.sun.mail</groupId>
            <artifactId>javax.mail</artifactId>
            <version>1.5.0</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.3.0</version>
            <type>jar</type>
        </dependency>        
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.11.3</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.11.3</version>
        </dependency>       
        <!-- Required so Verdi can compile in JDK's higher than 1.8 -->         
        <dependency> 
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>        
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <!--<scope>provided</scope>-->
        </dependency>        
    </dependencies>
    <build>
        <plugins>            
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId> 
                <version>2.5</version>       
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <mainClass>verishare.App</mainClass>
                        </manifest>
                    </archive>
                </configuration>        
            </plugin>
            
            <plugin>                
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.5.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>verishare.App</mainClass>
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>  <!--this is used for inheritance merges-->
                        <phase>package</phase>  <!--bind to the packaging phase -->
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>            
        </plugins>    
        <resources>
            <resource>
                <directory>src/main/webapp</directory>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
    </build>
</project>

Here's how I start Jetty:

public boolean startJetty(Server server) throws Exception, InterruptedException {
        boolean retVal = false;

        try {            
            server = new Server(AppSettings.getJettyServerPort());

            jettyServer = server;       

            server.setAttribute("org.eclipse.jetty.server.Request.maxFormContentSize", -1);                      

            String webDir = this.getClass().getClassLoader().getResource("static").toExternalForm();            

            SecurityHandler basicSecurity = getBasicAuthHandler("abc", "def");            

            WebAppContext waContext = new WebAppContext(webDir, "/");            
            waContext.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false");
            waContext.setSecurityHandler(basicSecurity);  
            
            waContext.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\.jar$|.*/[^/]*taglibs.*\\.jar$");                            
            
            ServletContextHandler servletContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
            servletContext.setMaxFormKeys(1000000000);
            servletContext.setContextPath("/api");
            servletContext.addServlet(new ServletHolder(new WebApiServlet()), "/*");

            String cn = "";

            Reflections rf = new Reflections("proj");
            Set<Class<?>> classWithPath = rf.getTypesAnnotatedWith(javax.ws.rs.Path.class);
            for (Class c : classWithPath) {
                if (cn.length() > 0) {
                    cn  = ";";
                }
                cn  = c.getCanonicalName();

                localLogger.info((String) logEntryRefNumLocal.get()   "Adding class: "   c.getCanonicalName());
            }
                        
           
            HandlerList handlers = new HandlerList();            
            handlers.setHandlers(new Handler[]{servletContext, waContext});

            server.setHandler(handlers);
            
            HttpConfiguration httpConfig = new HttpConfiguration();
            httpConfig.setSendServerVersion(false);
            HttpConnectionFactory httpFactory = new HttpConnectionFactory(httpConfig);
            ServerConnector httpConnector = new ServerConnector(server, httpFactory);
            httpConnector.setPort(AppSettings.getJettyServerPort());
            server.setConnectors(new Connector[]{httpConnector});

            server.setStopAtShutdown(true);
            server.setStopTimeout(0x2710L);

            server.start();

        } catch (InterruptedException iex) {
            localLogger.error((String) logEntryRefNumLocal.get()   "InterrupedException in WebHost.java startJetty method.", iex);

            retVal = false; 

            throw iex;
        } catch (RuntimeException rex) {
            localLogger.error((String) logEntryRefNumLocal.get()   "Runtime  exception in WebHost.java startJetty method.", rex);

            retVal = false;

            throw rex;
        } catch (Exception ex) {            
            localLogger.error((String) logEntryRefNumLocal.get()   "General exception in WebHost.java startJetty method.", ex);

            retVal = false;

            throw ex;
        }

        return retVal;
    }

If I run the above in Windows inside Netbeans 14, I can see the content of the .html file in /usr/src/webapp/static by visiting

http://127.0.0.1:8086/index.html

However, if I run the .jar in Ubuntu under the same version of the official Oracle JDK (JDK 17) using

/usr/lib/jvm/jdk-17/bin/java -Djavax.net.ssl.trustStore=/usr/lib/jvm/java-17-openjdk-amd64/lib/security/cacerts -cp /usr/src/verdi/verdi-12-JDK17-jar-with-dependencies.jar verishare.App

and I try to visit

http://172.16.1.33/index.html

Jetty 11.0.11 replies

HTTP ERROR 404 Not Found
URI:    /
STATUS: 404
MESSAGE:    Not Found
SERVLET:    org.eclipse.jetty.servlet.ServletHandler$Default404Servlet-726a17c4

What am I doing wrong that the above works in Windows inside NetBeans 14 with JDK 17, but it gives a 404 error with the compiled JAR in Linux (Ubuntu 20.04 LTS) with JDK 17?

Thanks

EDIT: I have refined the issue further. When run under Windows inside NetBeans 14 in JDK 17, in the code above, the "webDir" becomes

file:/D:/Projects/verdi_2/target/classes/static

When run under Linux in JDK 17, in the code above, the "webDir" becomes

jar:file:/usr/src/verdi/verdi-12-JDK17-jar-with-dependencies.jar!/static

It appears that the Linux versions of neither OpenJDK 17 nor the Oracle JDK 17, can interpret the above jar:file reference, leading to the 404.

How can this be fixed, though??? It is as if jar:file is completely opaque to Linux JREs of either flavor. No exceptions are raised, and I have confirmed over and over that the directory /static exists in the .JAR concerned.

EDIT: As I understand the answers below, this is then clearly invalid and the wrong way to access a directory full of html, Javascript, and images in a Jetty webAppContext that Jetty needs to serve to browser:

jar:file:/usr/src/verdi/verdi-12-JDK17-jar-with-dependencies.jar!/static

I still have no idea how to correct this, but can someone maybe comment: if the above is INCORRECT, what would a correct reference look like for a /static folder in the root of a JAR file?

EDIT: I have confirmed that if I regress the Jetty version back far enough to older versions, the reference jar:file:/usr/src/verdi/verdi-12-JDK17-jar-with-dependencies.jar!/static -does- start working and functions perfectly, and the older Jetty instance can serve the resources in the /static folder to any web-browsers that request them

CodePudding user response:

this.getClass().getClassLoader().getResource("static").toExternalForm();

This code has all sorts of massive problems with it. It's not supposed to work, but it's crazy enough that depending on a ton of tricky, hard to guarantee caveats, it will work. The usual solution to 'hey this code is incredibly fragile and if so much as the phase of the moon changes, this code is likely to break, please enumerate all the conditions I need to ensure so that it does not' is to say: "Forget that, write it differently, this is too fragile to continue to use!".

The simple stuff

Let's first fix the simple things and explain how this works. These don't immediately solve the problem, but are steps on the way to properly solve the problem.

this.getClass() is bad.

getClass() gets you your actual class. This is bad - the point of this class is to serve as 'context' for where to look for a given resource. Your intent is clearly is to refer to the very class whose definition is the source file you are writing for. In other words, if you have class Foo { ..... getClass().getResource... } your intent is to look up whatever resource you're looking up in the same place Foo.class is at, as you're writing in Foo.

That is not what getClass() actually does. If someone subclasses Foo, then getClass() would give you that class. In modular setups, that context is unlikely to work. This is needlessly fragile. The fix is to write Foo.class (exotic, but valid java: Gets you a reference to a class), and not getClass().

getClassLoader() is bad

Mostly, it's needless: the class object itself already has a .getResource method, there is no need to type .getClassLoader().getResource. In addition, getClassLoader() returns null (thus, causing NullPointerException) in exotic cases. Why limit your code so that it breaks in certain scenarios for no good reason?

There is one slight difference: someClass.getClassLoader().getResource resolves the stated resource relative to the root of the classpath. Whereas someClass.getResource includes the 'package' of the someClass as prefix. You can tell getResource not to do that by prefixing a slash. so, someClass.getClassLoader().getResource("static") can be simplified and improved to someClass.getResource("/static").

So far we're at..

ClassThisWasIn.class.getResource("/static") instead of this.getClass().getClassLoader().getResource("static")

Now for the hard part

The class loader resource stuff is an abstraction for the concept of loading resources (in that sense ClassLoader is a misnomer: a ClassLoader can load any resource; classes are just one kind of resource. ResourceLoader is the correct name, but java does not break backwards compat lightly, so once this thing was released with the ClassLoader name, fixing that brainfart became too much cost for the meagre gain).

As with any abstraction, the higher level you keep the abstraction the more bizarre scenarios can be represented by it. As a consequence, even though e.g. directories and jar files, which is how 99.9% of 'java resources' are shipped, all clearly have the concept of 'listing the contents' (you can ls a directory, you can jar tvf or unzip -l a jar file), the ClassLoader abstraction DOES NOT. Same applies to directories, and this is crucial: The ClassLoader abstraction simply does not have any concept about directories whatsoever. Hence, assuming /static is a dir, this entire principle is a hack that the spec does not guarantee will ever work.

That's in large part the problem here, but partly the authors of jetty are to blame, perhaps, for making you think you can write code like this without breaking spec.

At any rate, this hackery goes some way to explain what's wrong: Given that the classloader infra doesn't have any concept of directories, asking for a directory as a resource doesn't actually give you anything useful.

We can trivially give this a quick test!

class Example {
  public static void main(String[] args) {
    System.out.println(String.class.getResource("/java/lang/String.class"));
  }
}

When you run this, it dutifully prints:

jrt:/java.base/java/lang/String.class

Which look weird perhaps, but jrt is referring to 'java run time', and the reason you get this instead of a jar or file URL is because the core classes, starting with Java9, are not in jars anymore. Go scan your JDK dir, you won't find it. Until Java8, that would print something like: jar:file:/absolute/path/to/your/JDK/classes/rt.jar!java/lang/String.class.

Now let's try to get the lang package:

class Example {
  public static void main(String[] args) {
    System.out.println(String.class.getResource("/java/lang"));
  }
}

This prints null. Because you can't do that.

Now, if we try this stuff on jar/file classpaths, even on modern JVMs, it does 'work':

class Test {
  public static void main(String[] args) {
    System.out.println(Test.class.getResource("/Test.class"));
    System.out.println(Test.class.getResource("/"));
  }
}

For me this prints:

file:/Users/rzwitserloot/test/Test.class
file:/Users/rzwitserloot/test/

But as the jrt: example showed, whilst that first result is as expected per the java specs, that second is a wild stab in the dark. It worked... more or less by accident.

Now to core problem

You're asking for a resource named /static anywhere in the root classpath of whatever classpath is responsible for finding the Test.class resource. Often, this 'root' is the entire classpath. You don't generally get into multiple separate roots unless you use module systems. Thus, the problem becomes clear: You're asking for any occurrence of a dir named 'static' in the root of any of your classpath path entries, so you're getting a random one, and on various JDKs, they ship their own /static, and thus, you get the wrong one.

Alternatively, because you're asking for a dir and that is not a valid principle, the getResource API is just giving you the first classpath path entry and sticking /static at the end of it. You're beyond what the spec guarantees, so if instead it played Beethoven's 5th from the speakers that would be weird but not a bug either. Optimally the java spec should be more clear about how directories operate (and probably say: They don't, and then stop returning anything but null, though that would break lots of existing code).

Solutions

Instead, ask for a resource (a file, not a directory) that you know exists and whose name is unique. Then use string manipulation to lop the file part off so you are now left with a directory.

Let's say you know that /static/header.png definitely exists. That name is likely to be unique, though I'd consider going with /name-of-my-site/static/header.png instead to increase the odds of this, or to put that static dir in your package structure, e.g. if your class has package com.stefan.myapp at the top (in MyApp.java), to put this stuff in /com/stefan/myapp/static/header.png and now you can refer to that using MyApp.class.getResource("static/header.png"), because without the prefix / in getResource it's relative to the package. And now you have a guarantee of unique names.

Now just lop off /header.png and voila.

How does it work?

What jetty does is this:

  • User agent asked for /res/images/foo.png
  • My config says that anything in /res is for static files, and these are to be found via the classloader at jar:file:/whatever/foo/bar.jar!/a/b/c.
  • So, I lop off /res, and append the rest straight onto that path, giving me jar:file:/whatever/foo/bar.jar!/a/b/c/images/foo.png. I will then just toss that straight at the classloader and I'll run with whatever it gives me. I don't care what these strings are, they can be floobargle:/hootenanny/ as far as jetty is concerned.

Hence, you're really passing a 'prefix', as in: Take any resource, prefix this, ask the classloader for the end result. Thus, using the classloader to find a known resource and then lopping that resource off of the end of the string exactly matches what jetty will do with this string.

  • Related