Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.springframework.http.server;

import java.nio.charset.Charset;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
Expand All @@ -33,10 +34,13 @@
import org.jspecify.annotations.Nullable;

import org.springframework.http.HttpHeaders;
import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;

/**
* {@code MultiValueMap} implementation for wrapping Servlet request headers.
Expand All @@ -48,6 +52,11 @@ final class ServletRequestHeadersAdapter implements MultiValueMap<String, String

private final HttpServletRequest request;

/**
* Cached Content-Type value with charset appended.
*/
private @Nullable String cachedContentType;


private ServletRequestHeadersAdapter(HttpServletRequest request) {
this.request = request;
Expand All @@ -56,6 +65,9 @@ private ServletRequestHeadersAdapter(HttpServletRequest request) {

@Override
public @Nullable String getFirst(String key) {
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(key)) {
return getContentType();
}
return this.request.getHeader(key);
}

Expand Down Expand Up @@ -126,6 +138,10 @@ public boolean containsValue(Object rawValue) {
@Override
public @Nullable List<String> get(Object key) {
if (key instanceof String headerName) {
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(headerName)) {
String contentType = getContentType();
return (contentType != null ? Collections.singletonList(contentType) : null);
}
Enumeration<String> values = this.request.getHeaders(headerName);
if (values.hasMoreElements()) {
String value = values.nextElement();
Expand Down Expand Up @@ -178,6 +194,44 @@ public Set<Entry<String, List<String>>> entrySet() {
throw httpHeadersMapException();
}

/**
* Return the Content-Type header value, appending the charset from
* {@link HttpServletRequest#getCharacterEncoding()} if the Content-Type does not
* already include a {@code charset} parameter and the media type is not
* {@code application/json}.
* <p>The computed value is cached to avoid repeated string building.
*/
private @Nullable String getContentType() {
String stringContentType = this.cachedContentType;
if (stringContentType != null) {
return stringContentType;
}

stringContentType = this.request.getContentType();
try {
MediaType contentType = stringContentType != null ? MediaType.parseMediaType(stringContentType) : null;
if (contentType != null && contentType.getCharset() == null) {
String requestEncoding = this.request.getCharacterEncoding();
if (StringUtils.hasLength(requestEncoding)) {
Charset charset = Charset.forName(requestEncoding);
Map<String, String> params = new LinkedCaseInsensitiveMap<>();
params.putAll(contentType.getParameters());
if (!MediaType.APPLICATION_JSON.equals(contentType)) {
params.put("charset", charset.toString());
}
MediaType mediaType = new MediaType(contentType.getType(), contentType.getSubtype(), params);
stringContentType = mediaType.toString();
}
}
}
catch (InvalidMediaTypeException ex) {
// Ignore: simply not exposing an invalid content type in HttpHeaders...
}

this.cachedContentType = stringContentType;
return stringContentType;
}

private static UnsupportedOperationException immutableRequestException() {
return new UnsupportedOperationException("Request headers are immutable");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,8 @@

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.StringUtils;

/**
Expand Down Expand Up @@ -160,33 +158,6 @@ public HttpHeaders getHeaders() {

// HttpServletRequest exposes some headers as properties:
// we should include those if not already present
try {
MediaType contentType = this.headers.getContentType();
if (contentType == null) {
String requestContentType = this.servletRequest.getContentType();
if (StringUtils.hasLength(requestContentType)) {
contentType = MediaType.parseMediaType(requestContentType);
if (contentType.isConcrete()) {
this.headers.setContentType(contentType);
}
}
}
if (contentType != null && contentType.getCharset() == null) {
String requestEncoding = this.servletRequest.getCharacterEncoding();
if (StringUtils.hasLength(requestEncoding)) {
Charset charset = Charset.forName(requestEncoding);
Map<String, String> params = new LinkedCaseInsensitiveMap<>();
params.putAll(contentType.getParameters());
params.put("charset", charset.toString());
MediaType mediaType = new MediaType(contentType.getType(), contentType.getSubtype(), params);
this.headers.setContentType(mediaType);
}
}
}
catch (InvalidMediaTypeException ex) {
// Ignore: simply not exposing an invalid content type in HttpHeaders...
}

if (this.headers.getContentLength() < 0) {
int requestContentLength = this.servletRequest.getContentLength();
if (requestContentLength != -1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import org.junit.jupiter.api.Test;

import org.springframework.http.HttpHeaders;
import org.springframework.util.MultiValueMap;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;

Expand All @@ -44,4 +45,38 @@ void caseSensitiveOverride() {
assertThat(headersAdapter.get("foo")).containsExactly("override value");
}

@Test // gh-36426
void contentTypeCharsetAppendedForTextType() {
request.setContentType("text/plain");
request.setCharacterEncoding("UTF-8");

assertThat(headersAdapter.getFirst(HttpHeaders.CONTENT_TYPE)).isEqualTo("text/plain;charset=UTF-8");
assertThat(headersAdapter.get(HttpHeaders.CONTENT_TYPE)).containsExactly("text/plain;charset=UTF-8");
}

@Test // gh-36426
void contentTypeCharsetNotAppendedForApplicationJson() {
request.setContentType("application/json");
request.setCharacterEncoding("UTF-8");

assertThat(headersAdapter.getFirst(HttpHeaders.CONTENT_TYPE)).isEqualTo("application/json");
assertThat(headersAdapter.get(HttpHeaders.CONTENT_TYPE)).containsExactly("application/json");
}

@Test // gh-36426
void contentTypeCharsetNotAppendedWhenAlreadyPresent() {
request.setContentType("text/plain;charset=ISO-8859-1");
request.setCharacterEncoding("UTF-8");

assertThat(headersAdapter.getFirst(HttpHeaders.CONTENT_TYPE)).isEqualTo("text/plain;charset=ISO-8859-1");
assertThat(headersAdapter.get(HttpHeaders.CONTENT_TYPE)).containsExactly("text/plain;charset=ISO-8859-1");
}

@Test // gh-36426
void contentTypeCharsetNotAppendedWhenNoEncoding() {
request.setContentType("text/plain");

assertThat(headersAdapter.getFirst(HttpHeaders.CONTENT_TYPE)).isEqualTo("text/plain");
assertThat(headersAdapter.get(HttpHeaders.CONTENT_TYPE)).containsExactly("text/plain");
}
}