From a74bbf773d7c20408260fd8ee4f8ae929edaf04b Mon Sep 17 00:00:00 2001 From: jnbdz Date: Sun, 9 Aug 2026 21:17:54 -0400 Subject: [PATCH] fix: extract path parameters correctly when the router is mounted as subrouter The position of a path parameter was calculated from the beginning of the OpenAPI path template and applied to the path of the incoming request. When the router built from the contract is mounted as a subrouter, the request path contains additional leading segments, so the calculated position pointed to the wrong segment and validation failed or, even worse, validated the wrong value. Since mounting a router can only prepend segments to the request path, the number of additional leading segments is now determined by comparing the number of segments of the request path with the number of segments of the path template, and the parameter position is shifted accordingly. For routers that are not mounted as subrouter the behaviour is unchanged. Fixes #120 --- .../openapi/validation/RequestUtils.java | 20 ++++++++++++---- .../tests/validation/RequestUtilsTest.java | 23 +++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/vertx/openapi/validation/RequestUtils.java b/src/main/java/io/vertx/openapi/validation/RequestUtils.java index 4276579..1845aba 100644 --- a/src/main/java/io/vertx/openapi/validation/RequestUtils.java +++ b/src/main/java/io/vertx/openapi/validation/RequestUtils.java @@ -79,8 +79,9 @@ public static Future extract(HttpServerRequest request, Oper headers.put(param.getName(), extractHeaders(request, param)); break; case PATH: - int segment = findPathSegment(operation.getAbsoluteOpenAPIPath(), param.getName()); - pathParams.put(param.getName(), extractPathParameters(param, request, segment)); + String templatePath = operation.getAbsoluteOpenAPIPath(); + int segment = findPathSegment(templatePath, param.getName()); + pathParams.put(param.getName(), extractPathParameters(request, segment, countPathSegments(templatePath))); break; case QUERY: query.put(param.getName(), extractQuery(request, param)); @@ -119,12 +120,17 @@ private static RequestParameter extractHeaders(HttpServerRequest request, Parame return new RequestParameterImpl(urlDecodeIfRequired(parameter, headerValue)); } - private static RequestParameter extractPathParameters(Parameter param, HttpServerRequest request, int segment) { + private static RequestParameter extractPathParameters(HttpServerRequest request, int segment, + int templatePathSegments) { String[] pathSegments = request.path().substring(1).split("/"); - if (pathSegments.length < segment) { + // The router the operation is attached to may be mounted as a subrouter. In that case the request path + // contains additional leading segments that are not part of the OpenAPI path template. + int mountOffset = Math.max(0, pathSegments.length - templatePathSegments); + int index = mountOffset + segment; + if (pathSegments.length < index) { return EMPTY; } - return new RequestParameterImpl(decodeUrl(pathSegments[segment - 1])); + return new RequestParameterImpl(decodeUrl(pathSegments[index - 1])); } /** @@ -172,6 +178,10 @@ public static int findPathSegment(String templatePath, String parameterName) { return (int) templatePath.subSequence(0, idx).chars().filter(c -> c == '/').count(); } + private static int countPathSegments(String templatePath) { + return (int) templatePath.chars().filter(c -> c == '/').count(); + } + static String urlDecodeIfRequired(Parameter param, String value) { boolean requiresDecoding = Boolean.TRUE.equals(param.getExtensions().getOrDefault(Parameter.EXTENSION_URLDECODE, false)); diff --git a/src/test/java/io/vertx/tests/validation/RequestUtilsTest.java b/src/test/java/io/vertx/tests/validation/RequestUtilsTest.java index 0a7d392..324f0bd 100644 --- a/src/test/java/io/vertx/tests/validation/RequestUtilsTest.java +++ b/src/test/java/io/vertx/tests/validation/RequestUtilsTest.java @@ -231,6 +231,29 @@ void testFindPathSegment(String templatePath, String parameterName, int expected Truth.assertThat(RequestUtils.findPathSegment(templatePath, parameterName)).isEqualTo(expected); } + private static Stream testExtractPathFromMountedRouter() { + return Stream.of( + Arguments.of("/my-service", "/test/5.7"), + Arguments.of("/my-service", "/test/5.7/"), + Arguments.of("/deeply/nested/mount/point", "/test/5.7")); + } + + @ParameterizedTest(name = "{index} Path parameter should be extracted from {0}{1}") + @MethodSource + @Timeout(value = 2, timeUnit = TimeUnit.SECONDS) + void testExtractPathFromMountedRouter(String mountPoint, String path, VertxTestContext testContext) { + Parameter parameter = mockParameter("foo", PATH, NUMBER, false); + Operation mockedOperation = mockOperation(parameter); + when(mockedOperation.getAbsoluteOpenAPIPath()).thenReturn("/test/{foo}"); + + createValidationHandler(params -> { + Truth.assertThat(params.getPathParameters().get(parameter.getName()).getString()).isEqualTo("5.7"); + testContext.completeNow(); + }, mockedOperation, testContext).compose( + v -> createRequest(HttpMethod.GET, mountPoint + path).map(HttpClientRequest::send)) + .onFailure(testContext::failNow); + } + @Test @Timeout(value = 2, timeUnit = TimeUnit.SECONDS) void testBodySupplier(VertxTestContext testContext) {