From 7be3a61a14eb8bce18bfce091b51961666d8e7a7 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 22 Sep 2026 16:56:40 +0200 Subject: [PATCH] ResponseStatusException should not override custom reason Prior to this commit, `ResponseStatusException` would resolve the "detail" part of a problem detail response from message codes only (default or custom ones). If the `reason` given as an argument to the exception was a custom message, it would be overwritten in the process. This commit ensures that we use custom reason messages when they don't resolve as message codes. Fixes gh-36984 --- .../web/server/ResponseStatusException.java | 27 +++++++++++-------- .../web/ErrorResponseExceptionTests.java | 22 +++++++++++++++ 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java b/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java index 01f5bdfd3f4..9996c24900f 100644 --- a/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java +++ b/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java @@ -17,6 +17,7 @@ package org.springframework.web.server; import java.util.Locale; +import java.util.Objects; import org.jspecify.annotations.Nullable; @@ -117,19 +118,23 @@ public class ResponseStatusException extends ErrorResponseException { @Override public ProblemDetail updateAndGetBody(@Nullable MessageSource messageSource, Locale locale) { - super.updateAndGetBody(messageSource, locale); - - // The reason may be a code (consistent with ResponseStatusExceptionResolver) - - if (messageSource != null && getReason() != null && getReason().equals(getBody().getDetail())) { - Object[] arguments = getDetailMessageArguments(messageSource, locale); - String resolved = messageSource.getMessage(getReason(), arguments, null, locale); - if (resolved != null) { - getBody().setDetail(resolved); + String reason = getReason(); + if (reason != null) { + boolean detailNotCustomized = reason.equals(getBody().getDetail()); + super.updateAndGetBody(messageSource, locale); + if (!detailNotCustomized) { + return getBody(); } + // The reason may itself be a code (consistent with ResponseStatusExceptionResolver) + String resolved = null; + if (messageSource != null) { + resolved = messageSource.getMessage(reason, getDetailMessageArguments(messageSource, locale), null, locale); + } + // set to the custom reason if no message was resolved + getBody().setDetail(Objects.requireNonNullElse(resolved, reason)); + return getBody(); } - - return getBody(); + return super.updateAndGetBody(messageSource, locale); } @Override diff --git a/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java b/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java index 6c90e76e62c..3b8fa068c35 100644 --- a/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java +++ b/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java @@ -424,6 +424,28 @@ class ErrorResponseExceptionTests { } } + @Test // gh-36984 + void responseStatusExceptionCustomReason() { + Locale locale = Locale.UK; + LocaleContextHolder.setLocale(locale); + + try { + String reason = "bad.request"; + String message = "Breaking Bad Request"; + StaticMessageSource messageSource = new StaticMessageSource(); + messageSource.addMessage(reason, locale, message); + + String customReason = "my custom reason"; + ResponseStatusException ex = new ResponseStatusException(HttpStatus.BAD_REQUEST, customReason); + + ProblemDetail problemDetail = ex.updateAndGetBody(messageSource, locale); + assertThat(problemDetail.getDetail()).isEqualTo(customReason); + } + finally { + LocaleContextHolder.resetLocaleContext(); + } + } + private void assertStatus(ErrorResponse ex, HttpStatus status) { ProblemDetail body = ex.getBody(); assertThat(ex.getStatusCode()).isEqualTo(status);