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) {