From 50a4c874feb823d3c705073bb87bd6e4520ed329 Mon Sep 17 00:00:00 2001 From: Max Freedom Pollard <272618364+MaxFreedomPollard@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:37:44 -0400 Subject: [PATCH] fix: read @ResponseStatus from the controller class A @ResponseStatus on a controller class applies to every handler method that controller declares, and Spring returns that status at runtime. Operations on such a controller were documented as 200 instead. GenericResponseService.evaluateResponseStatus takes the method and the handler bean type, and falls back to the bean type when the method itself carries no @ResponseStatus, the same order Spring's own HandlerMethod uses. All four call sites passed methodParameter.getMethod().getClass(), which is java.lang.reflect.Method rather than the controller, so that fallback could never match. They now pass methodParameter.getContainingClass(), which is the handler bean type for a MethodParameter taken from a HandlerMethod and the declaring class otherwise. The guard in buildApiResponses that appends the @ResponseStatus code next to explicit @ApiResponse entries looks at the controller class as well. --- .../core/service/GenericResponseService.java | 13 ++--- .../api/v30/app273/HelloController.java | 45 +++++++++++++++++ .../api/v30/app273/SpringDocApp273Test.java | 33 ++++++++++++ .../api/v31/app273/HelloController.java | 45 +++++++++++++++++ .../api/v31/app273/SpringDocApp273Test.java | 33 ++++++++++++ .../test/resources/results/3.0.1/app273.json | 50 +++++++++++++++++++ .../test/resources/results/3.1.0/app273.json | 50 +++++++++++++++++++ 7 files changed, 263 insertions(+), 6 deletions(-) create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app273/HelloController.java create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app273/SpringDocApp273Test.java create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app273/HelloController.java create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app273/SpringDocApp273Test.java create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app273.json create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app273.json diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericResponseService.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericResponseService.java index 68e66155d..793b12042 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericResponseService.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericResponseService.java @@ -101,7 +101,7 @@ /** * The type Generic response builder. * - * @author bnasslahsen + * @author bnasslahsen, Max Freedom Pollard */ public class GenericResponseService implements ApplicationContextAware { @@ -450,7 +450,7 @@ private void buildGenericApiResponses(Components components, MethodParameter met else { // Use response parameters with no description filled - No documentation // available - String httpCode = evaluateResponseStatus(methodParameter.getMethod(), Objects.requireNonNull(methodParameter.getMethod()).getClass(), true); + String httpCode = evaluateResponseStatus(methodParameter.getMethod(), methodParameter.getContainingClass(), true); if (Objects.nonNull(httpCode)) { apiResponse = methodAttributes.getGenericMapResponse().containsKey(httpCode) ? methodAttributes.getGenericMapResponse().get(httpCode) : new ApiResponse(); @@ -491,16 +491,17 @@ private void buildApiResponses(Components components, MethodParameter methodPara buildApiResponses(components, methodParameter, apiResponsesOp, methodAttributes, httpCode, apiResponse, false); } } - if (AnnotatedElementUtils.hasAnnotation(methodParameter.getMethod(), ResponseStatus.class)) { + if (AnnotatedElementUtils.hasAnnotation(methodParameter.getMethod(), ResponseStatus.class) + || AnnotatedElementUtils.hasAnnotation(methodParameter.getContainingClass(), ResponseStatus.class)) { // Handles the case with @ResponseStatus, if the specified response is not already handled explicitly - String httpCode = evaluateResponseStatus(methodParameter.getMethod(), Objects.requireNonNull(methodParameter.getMethod()).getClass(), false); + String httpCode = evaluateResponseStatus(methodParameter.getMethod(), methodParameter.getContainingClass(), false); if (Objects.nonNull(httpCode) && !apiResponsesOp.containsKey(httpCode) && !apiResponsesOp.containsKey(ApiResponses.DEFAULT)) { buildApiResponses(components, methodParameter, apiResponsesOp, methodAttributes, httpCode, new ApiResponse(), false); } } } else { - String httpCode = evaluateResponseStatus(methodParameter.getMethod(), Objects.requireNonNull(methodParameter.getMethod()).getClass(), false); + String httpCode = evaluateResponseStatus(methodParameter.getMethod(), methodParameter.getContainingClass(), false); if (Objects.nonNull(httpCode)) buildApiResponses(components, methodParameter, apiResponsesOp, methodAttributes, httpCode, new ApiResponse(), false); } @@ -812,7 +813,7 @@ private boolean isValidHttpCode(String httpCode, MethodParameter methodParameter if (isHttpCodePresent(httpCode, responseSet)) result = true; } - if (httpCode.equals(evaluateResponseStatus(method, method.getClass(), false))) + if (httpCode.equals(evaluateResponseStatus(method, methodParameter.getContainingClass(), false))) result = true; } } diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app273/HelloController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app273/HelloController.java new file mode 100644 index 000000000..08155f545 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app273/HelloController.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019-2026 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.api.v30.app273; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +/** + * A controller that sets a status code for all of its handler methods, and one method that + * sets its own. + * + * @author Max Freedom Pollard + */ +@RestController +@ResponseStatus(HttpStatus.CREATED) +public class HelloController { + + @PostMapping("/hello") + public String hello() { + return null; + } + + @GetMapping("/bye") + @ResponseStatus(HttpStatus.ACCEPTED) + public String bye() { + return null; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app273/SpringDocApp273Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app273/SpringDocApp273Test.java new file mode 100644 index 000000000..2f7be7d18 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app273/SpringDocApp273Test.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019-2026 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.api.v30.app273; + +import test.org.springdoc.api.v30.AbstractSpringDocV30Test; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * A @ResponseStatus on the controller class applies to every handler method it declares. + * + * @author Max Freedom Pollard + */ +public class SpringDocApp273Test extends AbstractSpringDocV30Test { + + @SpringBootApplication + static class SpringDocTestApp { + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app273/HelloController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app273/HelloController.java new file mode 100644 index 000000000..19f8d0766 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app273/HelloController.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019-2026 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.api.v31.app273; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +/** + * A controller that sets a status code for all of its handler methods, and one method that + * sets its own. + * + * @author Max Freedom Pollard + */ +@RestController +@ResponseStatus(HttpStatus.CREATED) +public class HelloController { + + @PostMapping("/hello") + public String hello() { + return null; + } + + @GetMapping("/bye") + @ResponseStatus(HttpStatus.ACCEPTED) + public String bye() { + return null; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app273/SpringDocApp273Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app273/SpringDocApp273Test.java new file mode 100644 index 000000000..a81dec257 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app273/SpringDocApp273Test.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019-2026 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.api.v31.app273; + +import test.org.springdoc.api.v31.AbstractSpringDocTest; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * A @ResponseStatus on the controller class applies to every handler method it declares. + * + * @author Max Freedom Pollard + */ +public class SpringDocApp273Test extends AbstractSpringDocTest { + + @SpringBootApplication + static class SpringDocTestApp { + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app273.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app273.json new file mode 100644 index 000000000..076f81df6 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app273.json @@ -0,0 +1,50 @@ +{ + "openapi" : "3.0.1", + "info" : { + "title" : "OpenAPI definition", + "version" : "v0" + }, + "servers" : [ { + "url" : "http://localhost", + "description" : "Generated server url" + } ], + "paths" : { + "/hello" : { + "post" : { + "tags" : [ "hello-controller" ], + "operationId" : "hello", + "responses" : { + "201" : { + "description" : "Created", + "content" : { + "*/*" : { + "schema" : { + "type" : "string" + } + } + } + } + } + } + }, + "/bye" : { + "get" : { + "tags" : [ "hello-controller" ], + "operationId" : "bye", + "responses" : { + "202" : { + "description" : "Accepted", + "content" : { + "*/*" : { + "schema" : { + "type" : "string" + } + } + } + } + } + } + } + }, + "components" : { } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app273.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app273.json new file mode 100644 index 000000000..072f4011f --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app273.json @@ -0,0 +1,50 @@ +{ + "openapi" : "3.1.0", + "info" : { + "title" : "OpenAPI definition", + "version" : "v0" + }, + "servers" : [ { + "url" : "http://localhost", + "description" : "Generated server url" + } ], + "paths" : { + "/hello" : { + "post" : { + "tags" : [ "hello-controller" ], + "operationId" : "hello", + "responses" : { + "201" : { + "description" : "Created", + "content" : { + "*/*" : { + "schema" : { + "type" : "string" + } + } + } + } + } + } + }, + "/bye" : { + "get" : { + "tags" : [ "hello-controller" ], + "operationId" : "bye", + "responses" : { + "202" : { + "description" : "Accepted", + "content" : { + "*/*" : { + "schema" : { + "type" : "string" + } + } + } + } + } + } + } + }, + "components" : { } +}