Repository: vert-x3/vertx-web
Issue: Subrouter fails if mounted on another subrouter on path with path params

### Version

4.3.0 and 4.3.1-SNAPSHOT

### Context

Follow up on #2136 which still fails in some cases.

### Do you have a reproducer?

Add this to [SubRouterTest](https://github.com/vert-x3/vertx-web/blob/master/vertx-web/src/test/java/io/vertx/ext/web/SubRouterTest.java):

```
  @Test // FAILS!
  public void testHierarchicalWithParamsSimple() throws Exception {

    Router restRouter = Router.router(vertx);
    Router productRouter = Router.router(vertx);
    Router instanceRouter = Router.router(vertx);

    router.route("/rest*").subRouter(restRouter);
    restRouter.route("/product*").subRouter(productRouter);
    productRouter.route("/:id*").subRouter(instanceRouter);
    instanceRouter.get("/").handler(ctx -> {
      // id is extracted from the root router
      assertEquals("123", ctx.pathParam("id"));
      ctx.response().end();
    });

    // router
    // /rest -> restRouter
    //      /product -> productRouter
    //            /:id -> instanceRouter
    //                  / -> OK

    testRequest(HttpMethod.GET, "/rest/product/123", 200, "OK");
  }
```

However, route is recognized if I add another path param to last sub-router:

```
  @Test // succeeds
  public void testHierarchicalWithParamsSimpleWithDummy() throws Exception {

    Router restRouter = Router.router(vertx);
    Router productRouter = Router.router(vertx);
    Router instanceRouter = Router.router(vertx);

    router.route("/rest*").subRouter(restRouter);
    restRouter.route("/product*").subRouter(productRouter);
    productRouter.route("/:id*").subRouter(instanceRouter);
    instanceRouter.get("/:foo").handler(ctx -> {
      // id is extracted from the root router
      assertEquals("123", ctx.pathParam("id"));
      assertEquals("bar", ctx.pathParam("foo"));
      ctx.response().end();
    });

    // router
    // /rest -> restRouter
    //      /product -> productRouter
    //            /:id -> instanceRouter
    //                  /:foo -> OK

    testRequest(HttpMethod.GET, "/rest/product/123/bar", 200, "OK");
  }
```
### Steps to reproduce

1. Add those tests to [SubRouterTest.java](https://github.com/vert-x3/vertx-web/blob/master/vertx-web/src/test/java/io/vertx/ext/web/SubRouterTest.java)
2. Run tests with `mvn -Dtest=SubRouterTest test -pl vertx-web`

### Extra

* OS: Ubuntu 21.10
* JVM: OpenJDK 64-Bit Server VM 11.0.14.1 2022-02-08

Changed files:
vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java, vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImplBase.java, vertx-web/src/test/java/io/vertx/ext/web/SubRouterTest.java, vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java, vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextInternal.java, vertx-web/src/main/java/io/vertx/ext/web/impl/RouterImpl.java, vertx-web/src/main/java/io/vertx/ext/web/impl/RouteState.java

diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RouteState.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RouteState.java
index b9833c6a3..7d54c0c76 100644
--- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RouteState.java
+++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RouteState.java
@@ -1024,7 +1024,7 @@ final class RouteState {
         }
 
         context.matchRest = -1;
-        context.matchNormalized = useNormalizedPath;
+        context.normalizedMatch = useNormalizedPath;
 
         if (m.groupCount() > 0) {
           if (!exactPath) {
diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RouterImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RouterImpl.java
index 1fe9a7370..7f5e344fc 100644
--- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RouterImpl.java
+++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RouterImpl.java
@@ -246,12 +246,14 @@ public class RouterImpl implements Router {
 
   @Override
   public void handleContext(RoutingContext ctx) {
-    new RoutingContextWrapper(getAndCheckRoutePath(ctx), state.getRoutes(), (RoutingContextInternal) ctx, this).next();
+    final RoutingContextInternal ctxi = (RoutingContextInternal) ctx;
+    new RoutingContextWrapper(getAndCheckRoutePath(ctxi), state.getRoutes(), ctxi, this).next();
   }
 
   @Override
   public void handleFailure(RoutingContext ctx) {
-    new RoutingContextWrapper(getAndCheckRoutePath(ctx), state.getRoutes(), (RoutingContextInternal) ctx, this).next();
+    final RoutingContextInternal ctxi = (RoutingContextInternal) ctx;
+    new RoutingContextWrapper(getAndCheckRoutePath(ctxi), state.getRoutes(), ctxi, this).next();
   }
 
   @Override
@@ -333,8 +335,7 @@ public class RouterImpl implements Router {
     return state.getErrorHandler(statusCode);
   }
 
-  private String getAndCheckRoutePath(RoutingContext routingContext) {
-    final RoutingContextImplBase ctx = (RoutingContextImplBase) routingContext;
+  private String getAndCheckRoutePath(RoutingContextInternal ctx) {
     final Route route = ctx.currentRoute();
 
     if (!route.isRegexPath()) {
@@ -347,18 +348,9 @@ public class RouterImpl implements Router {
       }
     }
     // regex
-    if (ctx.matchRest != -1) {
+    if (ctx.restIndex() != -1) {
       // if we're on a sub router already we need to skip the matched path
-      int skip = ctx.mountPoint != null ? ctx.mountPoint.length() : 0;
-      if (ctx.matchNormalized) {
-        return ctx.normalizedPath().substring(skip, skip + ctx.matchRest);
-      } else {
-        String path = ctx.request().path();
-        if (path != null) {
-          return path.substring(skip, skip + ctx.matchRest);
-        }
-        return null;
-      }
+      return ctx.basePath();
     } else {
       // failure did not match
       throw new IllegalStateException("Sub routers must be mounted on paths (constant or parameterized)");
diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java
index c072c25ae..70d3628a7 100644
--- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java
+++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java
@@ -286,6 +286,16 @@ public class RoutingContextDecorator implements RoutingContextInternal {
     decoratedContext.setSession(session);
   }
 
+  @Override
+  public int restIndex() {
+    return decoratedContext.restIndex();
+  }
+
+  @Override
+  public boolean normalizedMatch() {
+    return decoratedContext.normalizedMatch();
+  }
+
   @Override
   public void setUser(User user) {
     decoratedContext.setUser(user);
diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java
index f043362c7..c6959902b 100644
--- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java
+++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java
@@ -108,7 +108,6 @@ public class RoutingContextImpl extends RoutingContextImplBase {
         HeaderParser.sort(HeaderParser.convertToParsedHeaderValues(acceptLanguage, ParsableLanguageValue::new)),
         new ParsableMIMEValue(contentType)
     );
-
   }
 
   @Override
diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImplBase.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImplBase.java
index 2a1261e0e..58ff9d1f5 100644
--- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImplBase.java
+++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImplBase.java
@@ -54,7 +54,7 @@ public abstract class RoutingContextImplBase implements RoutingContextInternal {
   int matchFailure;
   // the current path matched string
   int matchRest = -1;
-  boolean matchNormalized;
+  boolean normalizedMatch;
   // internal runtime state
   private volatile long seen;
 
@@ -78,6 +78,16 @@ public abstract class RoutingContextImplBase implements RoutingContextInternal {
     return (seen & id) != 0;
   }
 
+  @Override
+  public int restIndex() {
+    return matchRest;
+  }
+
+  @Override
+  public boolean normalizedMatch() {
+    return normalizedMatch;
+  }
+
   @Override
   public synchronized RoutingContextInternal setMatchFailure(int matchFailure) {
     this.matchFailure = matchFailure;
diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextInternal.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextInternal.java
index 34304fed4..f5a8b6b69 100644
--- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextInternal.java
+++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextInternal.java
@@ -84,4 +84,25 @@ public interface RoutingContextInternal extends RoutingContext {
    * @param session  the session
    */
   void setSession(Session session);
+
+  int restIndex();
+
+  boolean normalizedMatch();
+
+  default String basePath() {
+    // if we're on a sub router already we need to skip the matched path
+    String mountPoint = mountPoint();
+
+    int skip = mountPoint != null ? mountPoint.length() : 0;
+    if (normalizedMatch()) {
+      return normalizedPath().substring(skip, skip + restIndex());
+    } else {
+      String path = request().path();
+      if (path != null) {
+        return path.substring(skip, skip + restIndex());
+      }
+      return null;
+    }
+
+  };
 }
diff --git a/vertx-web/src/test/java/io/vertx/ext/web/SubRouterTest.java b/vertx-web/src/test/java/io/vertx/ext/web/SubRouterTest.java
index 4d66c6b2c..6d32d22e4 100644
--- a/vertx-web/src/test/java/io/vertx/ext/web/SubRouterTest.java
+++ b/vertx-web/src/test/java/io/vertx/ext/web/SubRouterTest.java
@@ -690,5 +690,55 @@ public class SubRouterTest extends WebTestBase {
 
     testRequest(HttpMethod.GET, "/rest/1/files/2", 200, "OK");
   }
-}
 
+  @Test
+  public void testHierarchicalWithParamsSimple() throws Exception {
+
+    Router restRouter = Router.router(vertx);
+    Router productRouter = Router.router(vertx);
+    Router instanceRouter = Router.router(vertx);
+
+    router.route("/rest*").subRouter(restRouter);
+    restRouter.route("/product*").subRouter(productRouter);
+    productRouter.route("/:id*").subRouter(instanceRouter);
+    instanceRouter.get("/").handler(ctx -> {
+      // id is extracted from the root router
+      assertEquals("123", ctx.pathParam("id"));
+      ctx.response().end();
+    });
+
+    // router
+    // /rest -> restRouter
+    //      /product -> productRouter
+    //            /:id -> instanceRouter
+    //                  / -> OK
+
+    testRequest(HttpMethod.GET, "/rest/product/123", 200, "OK");
+  }
+
+  @Test
+  public void testHierarchicalWithParamsSimpleWithDummy() throws Exception {
+
+    Router restRouter = Router.router(vertx);
+    Router productRouter = Router.router(vertx);
+    Router instanceRouter = Router.router(vertx);
+
+    router.route("/rest*").subRouter(restRouter);
+    restRouter.route("/product*").subRouter(productRouter);
+    productRouter.route("/:id*").subRouter(instanceRouter);
+    instanceRouter.get("/:foo").handler(ctx -> {
+      // id is extracted from the root router
+      assertEquals("123", ctx.pathParam("id"));
+      assertEquals("bar", ctx.pathParam("foo"));
+      ctx.response().end();
+    });
+
+    // router
+    // /rest -> restRouter
+    //      /product -> productRouter
+    //            /:id -> instanceRouter
+    //                  /:foo -> OK
+
+    testRequest(HttpMethod.GET, "/rest/product/123/bar", 200, "OK");
+  }
+}