diff --git a/server/src/dev/resources/application.yml b/server/src/dev/resources/application.yml index 0ba9f1a11..c2913e9b0 100644 --- a/server/src/dev/resources/application.yml +++ b/server/src/dev/resources/application.yml @@ -96,6 +96,10 @@ management: springdoc: model-and-view-allowed: true + api-docs: + # springdoc 3.x defaults to OpenAPI 3.1, whose mapper in swagger-core writes a single schema type as + # a bare string but cannot read one back, so springdoc warns on every schema it tries to clone. + version: openapi_3_0 swagger-ui: path: /swagger-ui docExpansion: list diff --git a/server/src/main/java/org/eclipse/openvsx/web/DocumentationConfig.java b/server/src/main/java/org/eclipse/openvsx/web/DocumentationConfig.java index 22954ea5b..f0dd05a32 100644 --- a/server/src/main/java/org/eclipse/openvsx/web/DocumentationConfig.java +++ b/server/src/main/java/org/eclipse/openvsx/web/DocumentationConfig.java @@ -17,6 +17,7 @@ import io.swagger.v3.oas.models.headers.Header; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.media.IntegerSchema; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.responses.ApiResponses; @@ -135,20 +136,26 @@ public OpenApiCustomizer sortSchemasAlphabetically() { }; } + /** + * Schemas must be built through the typed subclasses rather than {@code new Schema<>().type(...)}. + * The latter only sets the legacy string {@code type}, which an OpenAPI 3.1 document does not + * serialize at all - springdoc 3.x emits 3.1 by default - leaving the header with no type. The + * typed subclasses populate both that field and the 3.1 {@code types} set. + */ @Bean public OpenApiCustomizer addRateLimitResponse() { var limitLimitHeader = new Header() .description("Number of requests that can be made in a given amount of time") - .schema(new Schema<>().type("integer").format("int32")); + .schema(new IntegerSchema().format("int32")); var limitRemainingHeader = new Header() .description("Remaining number of requests left in the current time window") - .schema(new Schema<>().type("integer").format("int32")); + .schema(new IntegerSchema().format("int32")); var limitResetHeader = new Header() .description("Number of seconds until the rate limit tokens will be fully filled to its maximum") - .schema(new Schema<>().type("integer").format("int32")); + .schema(new IntegerSchema().format("int32")); var retryAfterHeader = new Header() .description("Number of seconds to wait after receiving a 429 response") - .schema(new Schema<>().type("integer").format("int32")); + .schema(new IntegerSchema().format("int32")); var response = new ApiResponse() .description("A client has sent too many requests in a given amount of time") diff --git a/server/src/test/java/org/eclipse/openvsx/web/DocumentationConfigTest.java b/server/src/test/java/org/eclipse/openvsx/web/DocumentationConfigTest.java index bd7ff1a16..b0dbead37 100644 --- a/server/src/test/java/org/eclipse/openvsx/web/DocumentationConfigTest.java +++ b/server/src/test/java/org/eclipse/openvsx/web/DocumentationConfigTest.java @@ -14,7 +14,12 @@ import java.lang.reflect.Method; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.core.util.Json31; +import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.Paths; import org.junit.jupiter.api.Test; import org.springframework.web.method.HandlerMethod; @@ -59,6 +64,29 @@ void shouldMarkAPreviewOperationOnlyOnce() { assertThat(operation.getDescription()).containsOnlyOnce("**Preview**"); } + // springdoc 3.x emits OpenAPI 3.1 by default, which does not serialize the legacy string `type` a + // bare `new Schema<>().type(...)` sets - the header then documents no type at all. The rate limit + // headers have to survive both spec versions, since which one is emitted is a springdoc default. + @Test + void shouldGiveTheRateLimitHeadersATypeInEitherSpecVersion() throws Exception { + var openApi = new OpenAPI().paths( + new Paths().addPathItem("/api/-/search", new PathItem().get(new Operation()))); + + new DocumentationConfig().addRateLimitResponse().customise(openApi); + + var schema = openApi.getPaths() + .get("/api/-/search") + .getGet() + .getResponses() + .get("429") + .getHeaders() + .get("X-RateLimit-Limit") + .getSchema(); + + assertThat(Json.mapper().writeValueAsString(schema)).contains("\"type\":\"integer\""); + assertThat(Json31.mapper().writeValueAsString(schema)).contains("\"type\":\"integer\""); + } + private void customize(Operation operation, String methodName) { Method method; try {