-
Bug
-
Resolution: Done
-
Major
-
1.2.3.Final
Sample Application to Reproduce
Here is a sample (albeit not the most reduced) project that reproduces the problem: https://github.com/shakuzen/spring-security-angular/tree/master/oauth2/ui
(You can run the 'ui' application using mvn spring-boot:run from /oauth2/ui relative to the project root which will run the Spring Boot application using an embedded Undertow version 1.2.3.Final)
Steps to Reproduce and Stacktrace
After the application starts, when you attempt to go to http://localhost:8080, Undertow will give the following error:
java.lang.StringIndexOutOfBoundsException: String index out of range: -4 at java.lang.String.substring(Unknown Source) at io.undertow.servlet.handlers.ServletPathMatch.<init>(ServletPathMatch.java:47) at io.undertow.servlet.handlers.ServletPathMatchesData.handleMatch(ServletPathMatchesData.java:88) at io.undertow.servlet.handlers.ServletPathMatchesData.getServletHandlerByPath(ServletPathMatchesData.java:75) at io.undertow.servlet.handlers.ServletPathMatches.getServletHandlerByPath(ServletPathMatches.java:83) at io.undertow.servlet.handlers.ServletInitialHandler.handleRequest(ServletInitialHandler.java:125) at io.undertow.server.handlers.HttpContinueReadHandler.handleRequest(HttpContinueReadHandler.java:65) at io.undertow.server.Connectors.executeRootHandler(Connectors.java:199) at io.undertow.server.protocol.http.HttpReadListener.handleEventWithNoRunningRequest(HttpReadListener.java:227) at io.undertow.server.protocol.http.HttpReadListener.handleEvent(HttpReadListener.java:128) at io.undertow.server.protocol.http.HttpOpenListener.handleEvent(HttpOpenListener.java:143) at io.undertow.server.protocol.http.HttpOpenListener.handleEvent(HttpOpenListener.java:90) at io.undertow.server.protocol.http.HttpOpenListener.handleEvent(HttpOpenListener.java:49) at org.xnio.ChannelListeners.invokeChannelListener(ChannelListeners.java:92) at org.xnio.ChannelListeners$10.handleEvent(ChannelListeners.java:291) at org.xnio.ChannelListeners$10.handleEvent(ChannelListeners.java:286) at org.xnio.ChannelListeners.invokeChannelListener(ChannelListeners.java:92) at org.xnio.nio.NioTcpServerHandle.handleReady(NioTcpServerHandle.java:53) at org.xnio.nio.WorkerThread.run(WorkerThread.java:539)
Root Cause Analysis
There are two servlets mapped by Spring as you can see in the log output:
2015-05-05 15:55:34.970 INFO 12656 --- [main] o.s.b.c.e.ServletRegistrationBean: Mapping servlet: 'zuulServlet' to [/zuul/*] 2015-05-05 15:55:34.977 INFO 12656 --- [main] o.s.b.c.e.ServletRegistrationBean: Mapping servlet: 'dispatcherServlet' to [/]
Then when ServletPathMatchesData.getServletHandlerByPath(String) is called ServletPathMatchesData.prefixMatches will have data like the following:
index | contents |
---|---|
0 | /zuul |
1 | io.undertow.util.SubstringMap$SubstringMatch@1001ea29 |
2 | "" |
3 | io.undertow.util.SubstringMap$SubstringMatch@54ad5475 |
Where indexes 1 and 3 are the SubstringMatch with PathMatch object containing the Zuul servlet and (default) dispatcherServlet respectively.
So far, things look alright. next it will make the call prefixMatches.get("/", 1) . This will return null because nothing in the above table matches "/".
Next it will make the call prefixMatches.get("/", 0, which will match the first thing in the table regardless of its key because it is a zero length substring match. It happens to be that the Zuul servlet is in the table before the default one with correct mapping "".
Lastly it tries to instantiate a SerlvetPathMatch with a uri of "/" but target.getServletPath() returns "/zuul" (because it is the Zuul Servlet that is erroneously chosen as the target) which causes the StringIndexOutOfBoundsException mentioned above when it attempts to do remaining = uri.substring(matched.length()); (line 47 of ServletPathMatch in Undertow 1.2.3.Final)
This issue does not happen on Undertow 1.1.x because it uses a regular Map (full string matches) instead of the custom-made SubstringMap:
final String part = path.substring(0, i); match = prefixMatches.get(part);
Which, in this described case, will do a full-string match (map lookup) for "" instead of a zero-length substring match that will match anything (thus returning the first thing it checks).
Possible solution
Perhaps for the case when length is 0 an exact check should be performed instead of returning the first key checked. This could be done internally in SubstringMap or by the places that call its methods. Either way, I think it would be good to document this behavior in the JavaDoc comments.
Side note
On 1.2.0.Final, you will not experience this issue but due to a different bug that appears to have been fixed. The table given above will only have one entry because the Zuul Servlet that originally gets input will be (partially) overwritten by the default dispatcherServlet. This is due to the logic in ServletPathMatchesData.Builder.addPrefixMatch() and again the zero-length substring match returning the first item it checks. It has been changed to use getExact in 1.2.3.Final.