From 2af86262019481dfe08d5e8ea7660ebbc5a2f773 Mon Sep 17 00:00:00 2001 From: Dmitry Lebedko Date: Sun, 4 Aug 2024 14:54:18 +0200 Subject: [PATCH 1/3] Replace swagger urls in `org.springdoc.core.properties.AbstractSwaggerUiConfigProperties.urls` only if url is changed. Test to check if the issue is fixed. Fixes #2509. --- .../springdoc/ui/AbstractSwaggerWelcome.java | 19 ++-- ...ltipleUrlsSeveralParallelRequestsTest.java | 89 +++++++++++++++++++ 2 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app8/SpringDocApp8MultipleUrlsSeveralParallelRequestsTest.java diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/ui/AbstractSwaggerWelcome.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/ui/AbstractSwaggerWelcome.java index 888e80a62..b8aaaede7 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/ui/AbstractSwaggerWelcome.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/ui/AbstractSwaggerWelcome.java @@ -33,6 +33,8 @@ import org.springframework.util.CollectionUtils; import org.springframework.web.util.UriComponentsBuilder; +import java.util.Objects; + import static org.springdoc.core.utils.Constants.SWAGGER_UI_OAUTH_REDIRECT_URL; import static org.springdoc.core.utils.Constants.SWAGGER_UI_URL; import static org.springframework.util.AntPathMatcher.DEFAULT_PATH_SEPARATOR; @@ -134,12 +136,17 @@ else if (swaggerUiConfigParameters.isValidUrl(swaggerUiUrl)) else swaggerUiConfigParameters.addUrl(apiDocsUrl); if (!CollectionUtils.isEmpty(swaggerUiConfig.getUrls())) { - swaggerUiConfig.cloneUrls().forEach(swaggerUrl -> { - swaggerUiConfigParameters.getUrls().remove(swaggerUrl); - if (!swaggerUiConfigParameters.isValidUrl(swaggerUrl.getUrl())) - swaggerUrl.setUrl(buildUrlWithContextPath(swaggerUrl.getUrl())); - swaggerUiConfigParameters.getUrls().add(swaggerUrl); - }); + swaggerUiConfig.cloneUrls() + .stream() + .filter(swaggerUrl -> !swaggerUiConfigParameters.isValidUrl(swaggerUrl.getUrl())) + .forEach(swaggerUrl -> { + final var url = buildUrlWithContextPath(swaggerUrl.getUrl()); + if (!Objects.equals(url, swaggerUrl.getUrl())) { + swaggerUiConfigParameters.getUrls().remove(swaggerUrl); + swaggerUrl.setUrl(url); + swaggerUiConfigParameters.getUrls().add(swaggerUrl); + } + }); } } calculateOauth2RedirectUrl(uriComponentsBuilder); diff --git a/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app8/SpringDocApp8MultipleUrlsSeveralParallelRequestsTest.java b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app8/SpringDocApp8MultipleUrlsSeveralParallelRequestsTest.java new file mode 100644 index 000000000..2b08b7038 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app8/SpringDocApp8MultipleUrlsSeveralParallelRequestsTest.java @@ -0,0 +1,89 @@ +/* + * + * * Copyright 2019-2020 the original author or authors. + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * https://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package test.org.springdoc.ui.app8; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.test.context.TestPropertySource; +import test.org.springdoc.ui.AbstractSpringDocTest; + +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +import static java.util.concurrent.CompletableFuture.allOf; +import static java.util.concurrent.CompletableFuture.runAsync; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * The test to make sure no race condition issues are present when several parallel requests + * are sent to get Swagger config. + * + * @author Dmitry Lebedko (lebedko.dmitrii@gmail.com) + */ +@TestPropertySource(properties = { + "springdoc.swagger-ui.urls[0].name=first-user-list", + "springdoc.swagger-ui.urls[0].url=/api-docs.yaml", + "springdoc.swagger-ui.urls[1].name=second-user-list", + "springdoc.swagger-ui.urls[1].url=/api-docs.yaml", + "springdoc.swagger-ui.urls[2].name=third-user-list", + "springdoc.swagger-ui.urls[2].url=/api-docs.yaml" +}) +public class SpringDocApp8MultipleUrlsSeveralParallelRequestsTest extends AbstractSpringDocTest { + + private static final int PARALLEL_REQUEST_NUMBER = 100; + + /** + * Sends {@link SpringDocApp8MultipleUrlsSeveralParallelRequestsTest#PARALLEL_REQUEST_NUMBER} requests + * simultaneously to make sure no race condition issues are present. + */ + @Test + public void swagger_config_for_multiple_groups_and_many_parallel_requests() { + assertDoesNotThrow(() -> { + allOf(Stream.generate(() -> runAsync(() -> { + try { + mockMvc.perform(get("/v3/api-docs/swagger-config")) + .andExpect(status().isOk()) + .andExpect(jsonPath("configUrl", equalTo("/v3/api-docs/swagger-config"))) + .andExpect(jsonPath("url").doesNotExist()) + .andExpect(jsonPath("urls.length()", equalTo(3))) + .andExpect(jsonPath("urls[0].url", equalTo("/api-docs.yaml"))) + .andExpect(jsonPath("urls[0].name", equalTo("first-user-list"))) + .andExpect(jsonPath("urls[1].url", equalTo("/api-docs.yaml"))) + .andExpect(jsonPath("urls[1].name", equalTo("second-user-list"))) + .andExpect(jsonPath("urls[2].url", equalTo("/api-docs.yaml"))) + .andExpect(jsonPath("urls[2].name", equalTo("third-user-list"))); + } catch (Exception e) { + throw new RuntimeException(e); + } + })) + .limit(PARALLEL_REQUEST_NUMBER) + .toArray(CompletableFuture[]::new)) + .join(); + }, "Swagger config is supposed to be delivered successfully " + + "no matter how many parallel requests are sent"); + } + + @SpringBootApplication + static class SpringDocTestApp {} + +} \ No newline at end of file From da0eb3e689b2754c65b0ac08e6a373f051977fc4 Mon Sep 17 00:00:00 2001 From: Dmitry Lebedko Date: Sun, 4 Aug 2024 16:09:42 +0200 Subject: [PATCH 2/3] Javadocs updated. --- .../SpringDocApp8MultipleUrlsSeveralParallelRequestsTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app8/SpringDocApp8MultipleUrlsSeveralParallelRequestsTest.java b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app8/SpringDocApp8MultipleUrlsSeveralParallelRequestsTest.java index 2b08b7038..76ffa29a8 100644 --- a/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app8/SpringDocApp8MultipleUrlsSeveralParallelRequestsTest.java +++ b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app8/SpringDocApp8MultipleUrlsSeveralParallelRequestsTest.java @@ -35,7 +35,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** - * The test to make sure no race condition issues are present when several parallel requests + * The test to make sure no exceptions are thrown when several parallel requests * are sent to get Swagger config. * * @author Dmitry Lebedko (lebedko.dmitrii@gmail.com) @@ -54,7 +54,7 @@ public class SpringDocApp8MultipleUrlsSeveralParallelRequestsTest extends Abstra /** * Sends {@link SpringDocApp8MultipleUrlsSeveralParallelRequestsTest#PARALLEL_REQUEST_NUMBER} requests - * simultaneously to make sure no race condition issues are present. + * simultaneously to make sure no exceptions are thrown when getting Swagger config. */ @Test public void swagger_config_for_multiple_groups_and_many_parallel_requests() { From 570e367b4ad4b51cb2efc80815b3a97aad8cba74 Mon Sep 17 00:00:00 2001 From: Dmitry Lebedko Date: Sun, 18 Aug 2024 15:20:12 +0200 Subject: [PATCH 3/3] Avoid mutation of `org.springdoc.core.properties.AbstractSwaggerUiConfigProperties.urls` and replace it with mutation of `org.springdoc.core.properties.AbstractSwaggerUiConfigProperties.SwaggerUrl.url` when necessary. Enhance `SpringDocApp8MultipleUrlsSeveralParallelRequestsTest` by specifying custom servlet path to check no exception is thrown. --- .../java/org/springdoc/ui/AbstractSwaggerWelcome.java | 7 ++++--- ...ngDocApp8MultipleUrlsSeveralParallelRequestsTest.java | 9 +++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/ui/AbstractSwaggerWelcome.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/ui/AbstractSwaggerWelcome.java index b8aaaede7..40b3e24d6 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/ui/AbstractSwaggerWelcome.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/ui/AbstractSwaggerWelcome.java @@ -142,9 +142,10 @@ else if (swaggerUiConfigParameters.isValidUrl(swaggerUiUrl)) .forEach(swaggerUrl -> { final var url = buildUrlWithContextPath(swaggerUrl.getUrl()); if (!Objects.equals(url, swaggerUrl.getUrl())) { - swaggerUiConfigParameters.getUrls().remove(swaggerUrl); - swaggerUrl.setUrl(url); - swaggerUiConfigParameters.getUrls().add(swaggerUrl); + swaggerUiConfigParameters.getUrls() + .stream() + .filter(swaggerUrl::equals) + .forEach(subUrl -> subUrl.setUrl(url)); } }); } diff --git a/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app8/SpringDocApp8MultipleUrlsSeveralParallelRequestsTest.java b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app8/SpringDocApp8MultipleUrlsSeveralParallelRequestsTest.java index 76ffa29a8..b2360c1b4 100644 --- a/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app8/SpringDocApp8MultipleUrlsSeveralParallelRequestsTest.java +++ b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app8/SpringDocApp8MultipleUrlsSeveralParallelRequestsTest.java @@ -41,6 +41,7 @@ * @author Dmitry Lebedko (lebedko.dmitrii@gmail.com) */ @TestPropertySource(properties = { + "spring.mvc.servlet.path=/servlet-path", "springdoc.swagger-ui.urls[0].name=first-user-list", "springdoc.swagger-ui.urls[0].url=/api-docs.yaml", "springdoc.swagger-ui.urls[1].name=second-user-list", @@ -63,14 +64,14 @@ public void swagger_config_for_multiple_groups_and_many_parallel_requests() { try { mockMvc.perform(get("/v3/api-docs/swagger-config")) .andExpect(status().isOk()) - .andExpect(jsonPath("configUrl", equalTo("/v3/api-docs/swagger-config"))) + .andExpect(jsonPath("configUrl", equalTo("/servlet-path/v3/api-docs/swagger-config"))) .andExpect(jsonPath("url").doesNotExist()) .andExpect(jsonPath("urls.length()", equalTo(3))) - .andExpect(jsonPath("urls[0].url", equalTo("/api-docs.yaml"))) + .andExpect(jsonPath("urls[0].url", equalTo("/servlet-path/api-docs.yaml"))) .andExpect(jsonPath("urls[0].name", equalTo("first-user-list"))) - .andExpect(jsonPath("urls[1].url", equalTo("/api-docs.yaml"))) + .andExpect(jsonPath("urls[1].url", equalTo("/servlet-path/api-docs.yaml"))) .andExpect(jsonPath("urls[1].name", equalTo("second-user-list"))) - .andExpect(jsonPath("urls[2].url", equalTo("/api-docs.yaml"))) + .andExpect(jsonPath("urls[2].url", equalTo("/servlet-path/api-docs.yaml"))) .andExpect(jsonPath("urls[2].name", equalTo("third-user-list"))); } catch (Exception e) { throw new RuntimeException(e);