From d4b2a493f98afcc1e75b7dd2772ed882c96de993 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 20 Feb 2026 10:45:22 +0000 Subject: [PATCH] Fix duplicate header writing in ResponseBodyEmitterReturnValueHandler Closes gh-36357 --- ...ResponseBodyEmitterReturnValueHandler.java | 7 +----- ...nseBodyEmitterReturnValueHandlerTests.java | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java index ac6c7526f4a..dd038afa785 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java @@ -214,12 +214,7 @@ public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodRetur else { emitter = this.reactiveHandler.handleValue(returnValue, returnType, contentType, mavContainer, webRequest); if (emitter == null) { - // We're not streaming; write headers without committing response - outputMessage.getHeaders().forEach((headerName, headerValues) -> { - for (String headerValue : headerValues) { - response.addHeader(headerName, headerValue); - } - }); + // reactive but not streaming, e.g. Mono or aggregated Flux return; } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandlerTests.java index c8fbbd59bda..a4f904aa432 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandlerTests.java @@ -30,6 +30,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; import reactor.core.scheduler.Schedulers; @@ -359,6 +360,27 @@ class ResponseBodyEmitterReturnValueHandlerTests { assertThat(this.response.getContentAsString()).isEqualTo("data:foo\n\ndata:bar\n\n"); } + @Test // gh-36357 + void responseEntityMono() throws Exception { + + ResponseEntity> entity = ResponseEntity.ok() + .contentType(MediaType.TEXT_PLAIN) + .header("X-Custom", "value") + .body(Mono.just("foo")); + + ResolvableType bodyType = forClassWithGenerics(Mono.class, String.class); + MethodParameter type = on(TestController.class).resolveReturnType(ResponseEntity.class, bodyType); + this.handler.handleReturnValue(entity, type, this.mavContainer, this.webRequest); + + assertThat(this.response.getStatus()).isEqualTo(200); + assertThat(this.response.getHeaders("Content-Type")).containsExactly("text/plain"); + assertThat(this.response.getHeaders("X-Custom")).containsExactly("value"); + + WebAsyncManager manager = WebAsyncUtils.getAsyncManager(request); + assertThat(manager.isConcurrentHandlingStarted()).isTrue(); + assertThat(manager.getConcurrentResult()).isEqualTo("foo"); + } + @SuppressWarnings({"unused", "ConstantConditions"}) private static class TestController { @@ -385,6 +407,8 @@ class ResponseBodyEmitterReturnValueHandlerTests { private ResponseEntity> h11() { return null; } + private ResponseEntity> h12() { return null; } + }