Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions server/src/dev/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down