quarkus: Hot reload fails with InvalidPathException on Windows

Starting with Quarkus 0.21.0 hot reload failes with InvalidPathException (at least) on Windows.

When a Quarkus application runs in dev mode, i. e. using mvn quarkus:dev, a REST call will trigger a hot reload, if one of the source files has changed before. That worked like a charm up to version 0.20.0. With 0.21.0 the REST response contains an error message:

`<header>

Error restarting Quarkus

<div class="exception-message">

java.nio.file.InvalidPathException: Illegal char <:> at index 2: /C:/Users/dw/.m2/repository/io/quarkus/arc/arc/0.21.0/arc-0.21.0.jar

</div>

</header>`

As the message complains about the ‘:’ in the file path, I guess, the problem arises on Windows only.

The bug can easily be reproduced:

  • Use any of the applications from the guide, e. g getting-started
  • Make sure that quarkus.version in pom.xml is 0.21.0
  • Build the application and start dev mode (mvn clean package quarkus:dev)
  • Make some code change
  • Call a REST endpoint (curl http://localhost:8080/hello)

My environment:

  • Windows 10 Pro
  • AdoptOpenJDK 8 (jdk8u202-b08) and 11 (jdk-11.0.3+7)

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 1
  • Comments: 79 (53 by maintainers)

Commits related to this issue

Most upvoted comments

I get the same error with 0.23.1 in Windows 10 and OpenJDK 11 with a fresh generated quarkus application. I just switched the version to 0.23.1: java.nio.file.InvalidPathException: Illegal char <:> at index 2: /C:/Users/Marcel/.m2/repository/io/quarkus/quarkus-netty/0.23.1/quarkus-netty-0.23.1.jar at java.base/sun.nio.fs.WindowsPathParser.normalize(WindowsPathParser.java:182) at java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:153) at java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:77) at java.base/sun.nio.fs.WindowsPath.parse(WindowsPath.java:92) at java.base/sun.nio.fs.WindowsFileSystem.getPath(WindowsFileSystem.java:229) at jdk.compiler/com.sun.tools.javac.file.FSInfo.getJarClassPath(FSInfo.java:112) at jdk.compiler/com.sun.tools.javac.file.CacheFSInfo.getJarClassPath(CacheFSInfo.java:93) at jdk.compiler/com.sun.tools.javac.file.Locations$SearchPath.addJarClassPath(Locations.java:423) at jdk.compiler/com.sun.tools.javac.file.Locations$SearchPath.addFile(Locations.java:413) at jdk.compiler/com.sun.tools.javac.file.Locations$SearchPath.addFiles(Locations.java:345) at jdk.compiler/com.sun.tools.javac.file.Locations$SearchPath.addFiles(Locations.java:352) at jdk.compiler/com.sun.tools.javac.file.Locations$SimpleLocationHandler.setPaths(Locations.java:724) at jdk.compiler/com.sun.tools.javac.file.Locations.setLocation(Locations.java:2098) at jdk.compiler/com.sun.tools.javac.file.JavacFileManager.setLocation(JavacFileManager.java:927) at io.quarkus.dev.JavaCompilationProvider.compile(JavaCompilationProvider.java:48) at io.quarkus.dev.ClassLoaderCompiler.compile(ClassLoaderCompiler.java:157) at io.quarkus.dev.RuntimeUpdatesProcessor.checkForChangedClasses(RuntimeUpdatesProc

OK, build finished, used @jaikiran 's version and now it worked for me!

Excellent! Thank you for testing this out. I’ll send a PR with that fix in a while.

I was reading some (java)docs of File/URL and there’s a very small mention about “standard case for drive letters” on Windows OS here[1]. So it does seem that the drive letter is playing a role here. I have a fix for it here https://github.com/jaikiran/quarkus/commit/57d31637bb3d41a8785e254c21c34eaacc935003. I just need to see how to deliver that to you for a quick test with instructions.

[1] https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/File.html#getCanonicalPath()

I submitted a JDK bug for the issue where it can’t understand drive letters in the URI. I don’t have a bug ID yet though.

@geoand - Yes, I’m experimenting a few ways to get past this, one of them being what @dmlloyd just suggested 😃 I’ll update this thread once I have something.

Partially correct. What seems to be happening is - there’s one (and just one I think) of those 100+ jars on the classpath which has a Class-Path manifest attribute of the form:

Class-Path: /C:/Users/Administrator/.m2/repository/io/quarkus/arc/arc/0.23.2/arc-0.23.2.jar /C:/Users/Administrator/.m2/repository/io/quarkus/quarkus-arc/0.23.2/quarkus-arc-0.23.2.jar

(rest of the entries snipped, since the pattern is similar)

That’s expected/normal. This is a relative URI (even though it’s an absolute path… a relative URI is one with no “scheme” component), so according to the JAR specification, this is allowed.

As you rightly note - (in recent versions of JDK), the com.sun.tools.javac.file.FSInfo#getJarClassPath uses FileSystems.getDefault().getPath(elt) (where elt is each entry in that Class-Path attribute). This getPath API doesn’t support the path parsing if it starts with the / character like above.

The only (I think) jar file which has this kind of Class-Path entries is our xxx-dev.jar file that we generate in the dev mojo in the code here https://github.com/quarkusio/quarkus/blob/master/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java#L495.

I’m not an expert when it comes to the URI API (it has always confused me and I have had to keep going back to experiemnts to understand how it behaves), but I’ve read the linked comment here #1673 (comment). I’m not too sure why the getRawPath is used. From what I have debugged so far on Windows, the getRawPath() actually returns (at least on Windows) a value of the form /C:/Users/Administrator/.m2/repository/io/quarkus/arc/arc/0.23.2/arc-0.23.2.jar (notice the starting /). Because of that, I haven’t seen it ever enter this if block here https://github.com/quarkusio/quarkus/blob/master/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java#L497. So, that Windows check is virtually a no-op. But of course, that isn’t causing the problem here - the usage of getRawPath seems to be the root cause. Are we using it so as to let it handle the space characters (by encoding it)?

Yes, it handles the URI encoding for us, essentially creating a relative URI as the JAR file specification demands. Note that the block you mentioned is not meant to be entered for the /C: case, it’s meant to be entered in the C: case (because C:/foo/bar is syntactically indistinguishable from an absolute URI, which is disallowed; by prepending a / we make it into a relative URI).

Keeping all this aside, I think this is a bug in the JDK code itself. Specifically, the public List<Path> getJarClassPath(Path file) throws IOException in com.sun.tools.javac.file.FSInfo is throwing an exception (java.nio.file.InvalidPathException) which isn’t declared in its throws clause. I think it should just catch this java.nio.file.InvalidPathException and rethrow it as an IOException so that callers of this getJarClassPath can then catch it and just log it and move on like in the case of Locations.addJarClassPath here https://github.com/openjdk/jdk/blob/master/src/jdk.compiler/share/classes/com/sun/tools/javac/file/Locations.java#L425. This is an internal API of the JDK, so I don’t know if this will be accepted as a bug, but I’ll raise it in the OpenJDK compiler-dev list and ask for inputs.

I think they will: the JAR specification does clearly state that the URI is relative. They should be treating the string as a URI rather than a filesystem path; that’s the beginning and end of the problem AFAICT.

@dmlloyd, I was able to get my hands on a Windows system and I was able to reproduce this consistently and narrow this down. An important higher level detail - this is reproducible only on recent (definitely on Java 11 but probably since Java 9?) JDK versions. It probably is because the code in recent JDK versions of com.sun.tools.javac.file.FSInfo#getJarClassPath changed from something like:

for (StringTokenizer st = new StringTokenizer(path); st.hasMoreTokens(); ) {
                String elt = st.nextToken();
                File f = (parent == null ? new File(elt) : new File(parent, elt));
                list.add(f);
            }

to

for (StringTokenizer st = new StringTokenizer(path);
                 st.hasMoreTokens(); ) {
                String elt = st.nextToken();
                Path f = FileSystems.getDefault().getPath(elt);
                if (!f.isAbsolute() && parent != null)
                    f = parent.resolve(f).toAbsolutePath();
                list.add(f);
            }

(usage of Path APIs)

More details follow:

(previously in this issue, you commented):

I think I figured out what’s happening.

The javac tool classes have their own handler for class paths. When you compile against a JAR on the class path, it apparently is recursively searching the manifests of each of those JARs and adding the dependency JARs as well.

From what I see in the JDK code, yes, this I believe is what’s happening.

(previously in this issue, you commented):

However one (or more) of the 100+ JAR files on the class path during this point has a Class-Path manifest attribute which contains a file: URL. But com.sun.tools.javac.file.FSInfo#getJarClassPath doesn’t support it: it doesn’t parse as a relative URI per spec, rather it just mashes it in as a path to the compiler.

Partially correct. What seems to be happening is - there’s one (and just one I think) of those 100+ jars on the classpath which has a Class-Path manifest attribute of the form:

Class-Path: /C:/Users/Administrator/.m2/repository/io/quarkus/arc/arc/0.23.2/arc-0.23.2.jar /C:/Users/Administrator/.m2/repository/io/quarkus/quarkus-arc/0.23.2/quarkus-arc-0.23.2.jar

(rest of the entries snipped, since the pattern is similar)

As you rightly note - (in recent versions of JDK), the com.sun.tools.javac.file.FSInfo#getJarClassPath uses FileSystems.getDefault().getPath(elt) (where elt is each entry in that Class-Path attribute). This getPath API doesn’t support the path parsing if it starts with the / character like above.

The only (I think) jar file which has this kind of Class-Path entries is our xxx-dev.jar file that we generate in the dev mojo in the code here https://github.com/quarkusio/quarkus/blob/master/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java#L495.

I’m not an expert when it comes to the URI API (it has always confused me and I have had to keep going back to experiemnts to understand how it behaves), but I’ve read the linked comment here https://github.com/quarkusio/quarkus/issues/1673#issuecomment-477205320. I’m not too sure why the getRawPath is used. From what I have debugged so far on Windows, the getRawPath() actually returns (at least on Windows) a value of the form /C:/Users/Administrator/.m2/repository/io/quarkus/arc/arc/0.23.2/arc-0.23.2.jar (notice the starting /). Because of that, I haven’t seen it ever enter this if block here https://github.com/quarkusio/quarkus/blob/master/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java#L497. So, that Windows check is virtually a no-op. But of course, that isn’t causing the problem here - the usage of getRawPath seems to be the root cause. Are we using it so as to let it handle the space characters (by encoding it)?

Keeping all this aside, I think this is a bug in the JDK code itself. Specifically, the public List<Path> getJarClassPath(Path file) throws IOException in com.sun.tools.javac.file.FSInfo is throwing an exception (java.nio.file.InvalidPathException) which isn’t declared in its throws clause. I think it should just catch this java.nio.file.InvalidPathException and rethrow it as an IOException so that callers of this getJarClassPath can then catch it and just log it and move on like in the case of Locations.addJarClassPath here https://github.com/openjdk/jdk/blob/master/src/jdk.compiler/share/classes/com/sun/tools/javac/file/Locations.java#L425. This is an internal API of the JDK, so I don’t know if this will be accepted as a bug, but I’ll raise it in the OpenJDK compiler-dev list and ask for inputs.

Problem still exists at Quarkus 0.21.2.

Error restarting Quarkus java.nio.file.InvalidPathException: Illegal char <:> at index 2: /C:/Users/Barbaros/.m2/repository/io/quarkus/arc/arc/0.21.2/arc-0.21.2.jar

java.nio.file.InvalidPathException: Illegal char <:> at index 2: /C:/Users/Barbaros/.m2/repository/io/quarkus/arc/arc/0.21.2/arc-0.21.2.jar at java.base/sun.nio.fs.WindowsPathParser.normalize(WindowsPathParser.java:182) at java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:153) at java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:77) at java.base/sun.nio.fs.WindowsPath.parse(WindowsPath.java:92) at java.base/sun.nio.fs.WindowsFileSystem.getPath(WindowsFileSystem.java:229) at jdk.compiler/com.sun.tools.javac.file.FSInfo.getJarClassPath(FSInfo.java:112) at jdk.compiler/com.sun.tools.javac.file.CacheFSInfo.getJarClassPath(CacheFSInfo.java:93) at jdk.compiler/com.sun.tools.javac.file.Locations$SearchPath.addJarClassPath(Locations.java:423) at jdk.compiler/com.sun.tools.javac.file.Locations$SearchPath.addFile(Locations.java:413) at jdk.compiler/com.sun.tools.javac.file.Locations$SearchPath.addFiles(Locations.java:345) at jdk.compiler/com.sun.tools.javac.file.Locations$SearchPath.addFiles(Locations.java:352) at jdk.compiler/com.sun.tools.javac.file.Locations$SimpleLocationHandler.setPaths(Locations.java:724) at jdk.compiler/com.sun.tools.javac.file.Locations.setLocation(Locations.java:2098) at jdk.compiler/com.sun.tools.javac.file.JavacFileManager.setLocation(JavacFileManager.java:927) at io.quarkus.dev.JavaCompilationProvider.compile(JavaCompilationProvider.java:48) at io.quarkus.dev.ClassLoaderCompiler.compile(ClassLoaderCompiler.java:156) at io.quarkus.dev.RuntimeUpdatesProcessor.checkForChangedClasses(RuntimeUpdatesProcessor.java:164) at io.quarkus.dev.RuntimeUpdatesProcessor.doScan(RuntimeUpdatesProcessor.java:103) at io.quarkus.undertow.deployment.devmode.UndertowHotReplacementSetup.handleHotDeploymentRequest(UndertowHotReplacementSetup.java:72) at io.quarkus.undertow.deployment.devmode.UndertowHotReplacementSetup$1$1.handleRequest(UndertowHotReplacementSetup.java:61) at io.undertow.server.Connectors.executeRootHandler(Connectors.java:376) at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:830) at io.quarkus.runtime.CleanableExecutor$CleaningRunnable.run(CleanableExecutor.java:224) at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) at org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35) at org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:2011) at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1535) at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1426) at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29) at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29) at java.base/java.lang.Thread.run(Thread.java:834) at org.jboss.threads.JBossThread.run(JBossThread.java:479)

Tested in;

  • Windows 10 Pro
  • OpenJDK Runtime Environment Corretto-11.0.4.11.1 (build 11.0.4+11-LTS)

No, I don’t think it’s the same issue as #3561. Looks like a different one.

@dirkweil could you provide the full stacktrace? No Windows box here 😃.

Thanks!