From a9f447e8d780a9eabe67c323da394c9b04a3503f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 17 Feb 2026 17:38:50 +0100 Subject: [PATCH] Consistent adaptation of HTTP headers on Servlet responses Includes use of Servlet 6.1 setCharacterEncoding(Charset) Closes gh-36343 --- .../http/server/ServletServerHttpRequest.java | 4 +- .../server/ServletServerHttpResponse.java | 27 +++++++++++++- .../reactive/ServletServerHttpResponse.java | 37 ++++++++++--------- .../function/AbstractServerResponse.java | 25 +++++++++---- ...ResponseBodyEmitterReturnValueHandler.java | 5 +++ .../view/script/ScriptTemplateView.java | 2 +- 6 files changed, 69 insertions(+), 31 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java index f7567c1f509..d2a2d3c32bb 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java @@ -174,10 +174,10 @@ public class ServletServerHttpRequest implements ServerHttpRequest { if (contentType != null && contentType.getCharset() == null) { String requestEncoding = this.servletRequest.getCharacterEncoding(); if (StringUtils.hasLength(requestEncoding)) { - Charset charSet = Charset.forName(requestEncoding); + Charset charset = Charset.forName(requestEncoding); Map params = new LinkedCaseInsensitiveMap<>(); params.putAll(contentType.getParameters()); - params.put("charset", charSet.toString()); + params.put("charset", charset.toString()); MediaType mediaType = new MediaType(contentType.getType(), contentType.getSubtype(), params); this.headers.setContentType(mediaType); } diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java index f7bda8df86d..2b4f9a55ade 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java @@ -24,6 +24,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; import org.springframework.util.Assert; /** @@ -87,13 +88,13 @@ public class ServletServerHttpResponse implements ServerHttpResponse { @Override public OutputStream getBody() throws IOException { this.bodyUsed = true; - this.headersWritten = true; + writeHeaders(); return this.servletResponse.getOutputStream(); } @Override public void flush() throws IOException { - this.headersWritten = true; + writeHeaders(); if (this.bodyUsed) { this.servletResponse.flushBuffer(); } @@ -101,6 +102,28 @@ public class ServletServerHttpResponse implements ServerHttpResponse { @Override public void close() { + writeHeaders(); + } + + private void writeHeaders() { + if (!this.headersWritten) { + // HttpServletResponse exposes some headers as properties: we should include those if not already present + if (this.servletResponse.getContentType() == null && this.headers.containsHeader(HttpHeaders.CONTENT_TYPE)) { + this.servletResponse.setContentType(this.headers.getFirst(HttpHeaders.CONTENT_TYPE)); + } + if (this.servletResponse.getCharacterEncoding() == null && this.headers.containsHeader(HttpHeaders.CONTENT_TYPE)) { + try { + // Lazy parsing into MediaType + MediaType contentType = this.headers.getContentType(); + if (contentType != null) { + this.servletResponse.setCharacterEncoding(contentType.getCharset()); + } + } + catch (Exception ex) { + // Leave character encoding unspecified + } + } + } this.headersWritten = true; } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 5c37d186c83..1d87e51dbef 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -18,7 +18,6 @@ package org.springframework.http.server.reactive; import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.charset.Charset; import jakarta.servlet.AsyncContext; import jakarta.servlet.AsyncEvent; @@ -122,31 +121,34 @@ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse { } protected void adaptHeaders(boolean removeAdaptedHeaders) { - MediaType contentType = null; - try { - contentType = getHeaders().getContentType(); - } - catch (Exception ex) { - String rawContentType = getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); - this.response.setContentType(rawContentType); - } - if (this.response.getContentType() == null && contentType != null) { - this.response.setContentType(contentType.toString()); + HttpHeaders headers = getHeaders(); + // HttpServletResponse exposes some headers as properties: we should include those if not already present + + if (this.response.getContentType() == null && headers.containsHeader(HttpHeaders.CONTENT_TYPE)) { + this.response.setContentType(headers.getFirst(HttpHeaders.CONTENT_TYPE)); } - Charset charset = (contentType != null ? contentType.getCharset() : null); - if (this.response.getCharacterEncoding() == null && charset != null) { - this.response.setCharacterEncoding(charset.name()); + if (this.response.getCharacterEncoding() == null && headers.containsHeader(HttpHeaders.CONTENT_TYPE)) { + try { + // Lazy parsing into MediaType + MediaType contentType = headers.getContentType(); + if (contentType != null) { + this.response.setCharacterEncoding(contentType.getCharset()); + } + } + catch (Exception ex) { + // Leave character encoding unspecified + } } - long contentLength = getHeaders().getContentLength(); + long contentLength = headers.getContentLength(); if (contentLength != -1) { this.response.setContentLengthLong(contentLength); } if (removeAdaptedHeaders) { - getHeaders().remove(HttpHeaders.CONTENT_TYPE); - getHeaders().remove(HttpHeaders.CONTENT_LENGTH); + headers.remove(HttpHeaders.CONTENT_TYPE); + headers.remove(HttpHeaders.CONTENT_LENGTH); } } @@ -352,7 +354,6 @@ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse { private class ResponseBodyProcessor extends AbstractListenerWriteProcessor { - public ResponseBodyProcessor() { super(request.getLogPrefix()); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AbstractServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AbstractServerResponse.java index 736bbea91fe..cad6d4f4257 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AbstractServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AbstractServerResponse.java @@ -29,6 +29,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -51,13 +52,13 @@ abstract class AbstractServerResponse extends ErrorHandlingServerResponse { private final MultiValueMap cookies; + protected AbstractServerResponse( HttpStatusCode statusCode, HttpHeaders headers, MultiValueMap cookies) { this.statusCode = statusCode; this.headers = HttpHeaders.readOnlyHttpHeaders(headers); - this.cookies = - CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(cookies)); + this.cookies = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(cookies)); } @Override @@ -110,14 +111,22 @@ abstract class AbstractServerResponse extends ErrorHandlingServerResponse { servletResponse.addHeader(headerName, headerValue); } }); + // HttpServletResponse exposes some headers as properties: we should include those if not already present - if (servletResponse.getContentType() == null && this.headers.getContentType() != null) { - servletResponse.setContentType(this.headers.getContentType().toString()); + if (servletResponse.getContentType() == null && this.headers.containsHeader(HttpHeaders.CONTENT_TYPE)) { + servletResponse.setContentType(this.headers.getFirst(HttpHeaders.CONTENT_TYPE)); } - if (servletResponse.getCharacterEncoding() == null && - this.headers.getContentType() != null && - this.headers.getContentType().getCharset() != null) { - servletResponse.setCharacterEncoding(this.headers.getContentType().getCharset().name()); + if (servletResponse.getCharacterEncoding() == null && this.headers.containsHeader(HttpHeaders.CONTENT_TYPE)) { + try { + // Lazy parsing into MediaType + MediaType contentType = this.headers.getContentType(); + if (contentType != null) { + servletResponse.setCharacterEncoding(contentType.getCharset()); + } + } + catch (Exception ex) { + // Leave character encoding unspecified + } } } 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 63915583dd2..ac6c7526f4a 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 @@ -458,6 +458,11 @@ public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodRetur // ignore } + @Override + public void setCharacterEncoding(Charset encoding) { + // ignore + } + @Override public void setContentLength(int len) { // ignore diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java index 3f6cc6344b7..fed025c8531 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java @@ -374,7 +374,7 @@ public class ScriptTemplateView extends AbstractUrlBasedView { setResponseContentType(request, response); if (this.charset != null) { - response.setCharacterEncoding(this.charset.name()); + response.setCharacterEncoding(this.charset); } }