Consistent adaptation of HTTP headers on Servlet responses

Closes gh-36345
This commit is contained in:
Juergen Hoeller
2026-02-17 18:40:50 +01:00
parent bfa81290b3
commit acab61f9a7
5 changed files with 78 additions and 37 deletions
@@ -187,10 +187,10 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
if (contentType != null && contentType.getCharset() == null) { if (contentType != null && contentType.getCharset() == null) {
String requestEncoding = this.servletRequest.getCharacterEncoding(); String requestEncoding = this.servletRequest.getCharacterEncoding();
if (StringUtils.hasLength(requestEncoding)) { if (StringUtils.hasLength(requestEncoding)) {
Charset charSet = Charset.forName(requestEncoding); Charset charset = Charset.forName(requestEncoding);
Map<String, String> params = new LinkedCaseInsensitiveMap<>(); Map<String, String> params = new LinkedCaseInsensitiveMap<>();
params.putAll(contentType.getParameters()); params.putAll(contentType.getParameters());
params.put("charset", charSet.toString()); params.put("charset", charset.toString());
MediaType mediaType = new MediaType(contentType.getType(), contentType.getSubtype(), params); MediaType mediaType = new MediaType(contentType.getType(), contentType.getSubtype(), params);
this.headers.setContentType(mediaType); this.headers.setContentType(mediaType);
} }
@@ -207,7 +207,6 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
} }
} }
} }
return this.headers; return this.headers;
} }
@@ -18,6 +18,7 @@ package org.springframework.http.server;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@@ -118,19 +119,31 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
this.servletResponse.addHeader(headerName, headerValue); this.servletResponse.addHeader(headerName, headerValue);
} }
}); });
// HttpServletResponse exposes some headers as properties: we should include those if not already present // HttpServletResponse exposes some headers as properties: we should include those if not already present
MediaType contentTypeHeader = this.headers.getContentType(); if (this.servletResponse.getContentType() == null && this.headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
if (this.servletResponse.getContentType() == null && contentTypeHeader != null) { this.servletResponse.setContentType(this.headers.getFirst(HttpHeaders.CONTENT_TYPE));
this.servletResponse.setContentType(contentTypeHeader.toString());
} }
if (this.servletResponse.getCharacterEncoding() == null && contentTypeHeader != null && if (this.servletResponse.getCharacterEncoding() == null && this.headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
contentTypeHeader.getCharset() != null) { try {
this.servletResponse.setCharacterEncoding(contentTypeHeader.getCharset().name()); // Lazy parsing into MediaType
MediaType contentType = this.headers.getContentType();
if (contentType != null) {
Charset charset = contentType.getCharset();
if (charset != null) {
this.servletResponse.setCharacterEncoding(charset.name());
}
}
}
catch (Exception ex) {
// Leave character encoding unspecified
}
} }
long contentLength = getHeaders().getContentLength(); long contentLength = this.headers.getContentLength();
if (contentLength != -1) { if (contentLength != -1) {
this.servletResponse.setContentLengthLong(contentLength); this.servletResponse.setContentLengthLong(contentLength);
} }
this.headersWritten = true; this.headersWritten = true;
} }
} }
@@ -50,7 +50,9 @@ import org.springframework.util.ReflectionUtils;
*/ */
class ServletServerHttpResponse extends AbstractListenerServerHttpResponse { class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
private static final boolean IS_SERVLET61 = ReflectionUtils.findField(HttpServletResponse.class, "SC_PERMANENT_REDIRECT") != null; private static final boolean SERVLET61 =
(ReflectionUtils.findField(HttpServletResponse.class, "SC_PERMANENT_REDIRECT") != null);
private final HttpServletResponse response; private final HttpServletResponse response;
@@ -138,31 +140,35 @@ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
} }
protected void adaptHeaders(boolean removeAdaptedHeaders) { protected void adaptHeaders(boolean removeAdaptedHeaders) {
MediaType contentType = null; HttpHeaders headers = getHeaders();
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());
}
Charset charset = (contentType != null ? contentType.getCharset() : null); // HttpServletResponse exposes some headers as properties: we should include those if not already present
if (this.response.getCharacterEncoding() == null && charset != null) { if (this.response.getContentType() == null && headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
this.response.setCharacterEncoding(charset.name()); this.response.setContentType(headers.getFirst(HttpHeaders.CONTENT_TYPE));
} }
if (this.response.getCharacterEncoding() == null && headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
long contentLength = getHeaders().getContentLength(); try {
// Lazy parsing into MediaType
MediaType contentType = headers.getContentType();
if (contentType != null) {
Charset charset = contentType.getCharset();
if (charset != null) {
this.response.setCharacterEncoding(charset.name());
}
}
}
catch (Exception ex) {
// Leave character encoding unspecified
}
}
long contentLength = headers.getContentLength();
if (contentLength != -1) { if (contentLength != -1) {
this.response.setContentLengthLong(contentLength); this.response.setContentLengthLong(contentLength);
} }
if (removeAdaptedHeaders) { if (removeAdaptedHeaders) {
getHeaders().remove(HttpHeaders.CONTENT_TYPE); headers.remove(HttpHeaders.CONTENT_TYPE);
getHeaders().remove(HttpHeaders.CONTENT_LENGTH); headers.remove(HttpHeaders.CONTENT_LENGTH);
} }
} }
@@ -186,7 +192,7 @@ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
cookie.setSecure(httpCookie.isSecure()); cookie.setSecure(httpCookie.isSecure());
cookie.setHttpOnly(httpCookie.isHttpOnly()); cookie.setHttpOnly(httpCookie.isHttpOnly());
if (httpCookie.isPartitioned()) { if (httpCookie.isPartitioned()) {
if (IS_SERVLET61) { if (SERVLET61) {
cookie.setAttribute("Partitioned", ""); cookie.setAttribute("Partitioned", "");
} }
else { else {
@@ -373,7 +379,6 @@ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
private class ResponseBodyProcessor extends AbstractListenerWriteProcessor<DataBuffer> { private class ResponseBodyProcessor extends AbstractListenerWriteProcessor<DataBuffer> {
public ResponseBodyProcessor() { public ResponseBodyProcessor() {
super(request.getLogPrefix()); super(request.getLogPrefix());
} }
@@ -17,6 +17,7 @@
package org.springframework.web.servlet.function; package org.springframework.web.servlet.function;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collection; import java.util.Collection;
import java.util.Set; import java.util.Set;
@@ -28,6 +29,7 @@ import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatusCode; import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
@@ -51,6 +53,7 @@ abstract class AbstractServerResponse extends ErrorHandlingServerResponse {
private final MultiValueMap<String, Cookie> cookies; private final MultiValueMap<String, Cookie> cookies;
protected AbstractServerResponse( protected AbstractServerResponse(
HttpStatusCode statusCode, HttpHeaders headers, MultiValueMap<String, Cookie> cookies) { HttpStatusCode statusCode, HttpHeaders headers, MultiValueMap<String, Cookie> cookies) {
@@ -60,6 +63,7 @@ abstract class AbstractServerResponse extends ErrorHandlingServerResponse {
CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(cookies)); CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(cookies));
} }
@Override @Override
public final HttpStatusCode statusCode() { public final HttpStatusCode statusCode() {
return this.statusCode; return this.statusCode;
@@ -118,14 +122,29 @@ abstract class AbstractServerResponse extends ErrorHandlingServerResponse {
servletResponse.addHeader(headerName, headerValue); servletResponse.addHeader(headerName, headerValue);
} }
}); });
// HttpServletResponse exposes some headers as properties: we should include those if not already present // HttpServletResponse exposes some headers as properties: we should include those if not already present
if (servletResponse.getContentType() == null && this.headers.getContentType() != null) { if (servletResponse.getContentType() == null && this.headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
servletResponse.setContentType(this.headers.getContentType().toString()); servletResponse.setContentType(this.headers.getFirst(HttpHeaders.CONTENT_TYPE));
} }
if (servletResponse.getCharacterEncoding() == null && if (servletResponse.getCharacterEncoding() == null && this.headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
this.headers.getContentType() != null && try {
this.headers.getContentType().getCharset() != null) { // Lazy parsing into MediaType
servletResponse.setCharacterEncoding(this.headers.getContentType().getCharset().name()); MediaType contentType = this.headers.getContentType();
if (contentType != null) {
Charset charset = contentType.getCharset();
if (charset != null) {
servletResponse.setCharacterEncoding(charset.name());
}
}
}
catch (Exception ex) {
// Leave character encoding unspecified
}
}
long contentLength = this.headers.getContentLength();
if (contentLength != -1) {
servletResponse.setContentLengthLong(contentLength);
} }
} }
@@ -459,6 +459,11 @@ public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodRetur
// ignore // ignore
} }
// @Override - on Servlet 6.1
public void setCharacterEncoding(Charset encoding) {
// ignore
}
@Override @Override
public void setContentLength(int len) { public void setContentLength(int len) {
// ignore // ignore