From 051219c3c9c58b511dba8d745ed7484240f2c39d Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 26 Mar 2026 15:07:48 +0100 Subject: [PATCH] Introduce HttpMessageConverter#canWriteRepeatedly The `AbstractHttpMessageConverter#supportsRepeatableWrites` contract is a protected method that message converters can override. This method tells whether the current converter can write several times the payload given as a parameter. This is mainly useful on the client side, where we need to know if we can send the same HTTP message again, after receiving an HTTP redirect status. Because this method is protected, this limits our ability to call it from a different package; this is needed for gh-33263. This commit promotes this method to the main `HttpMessageConverter` interface and deprecates the former. Closes gh-36252 --- .../AbstractGenericHttpMessageConverter.java | 2 +- .../AbstractHttpMessageConverter.java | 4 ++- .../AbstractJacksonHttpMessageConverter.java | 6 ++++ ...tlinSerializationHttpMessageConverter.java | 6 ++++ .../AbstractSmartHttpMessageConverter.java | 2 +- .../ByteArrayHttpMessageConverter.java | 6 ++++ .../converter/FormHttpMessageConverter.java | 9 +++--- .../http/converter/HttpMessageConverter.java | 18 +++++++++++ .../ObjectToStringHttpMessageConverter.java | 6 ++++ .../ResourceHttpMessageConverter.java | 8 ++++- .../ResourceRegionHttpMessageConverter.java | 32 +++++++++++-------- .../converter/StringHttpMessageConverter.java | 6 ++++ .../AbstractWireFeedHttpMessageConverter.java | 7 ++++ .../AbstractJackson2HttpMessageConverter.java | 6 ++++ .../json/GsonHttpMessageConverter.java | 6 ++++ .../json/JsonbHttpMessageConverter.java | 6 ++++ .../ProtobufHttpMessageConverter.java | 7 ++++ .../Jaxb2RootElementHttpMessageConverter.java | 6 ++++ .../xml/MarshallingHttpMessageConverter.java | 6 ++++ .../xml/SourceHttpMessageConverter.java | 8 ++++- 20 files changed, 134 insertions(+), 23 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java index a72f1ab1873..e80ddc4a330 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java @@ -117,7 +117,7 @@ public abstract class AbstractGenericHttpMessageConverter extends AbstractHtt } @Override public boolean repeatable() { - return supportsRepeatableWrites(t); + return canWriteRepeatedly(t, contentType); } }); } diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java index c1f587eecc8..1c6b5fee0a3 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java @@ -304,9 +304,11 @@ public abstract class AbstractHttpMessageConverter implements HttpMessageConv * @return {@code true} if {@code t} can be written repeatedly; * {@code false} otherwise * @since 6.1 + * @deprecated since 7.1 in favor of {@link #canWriteRepeatedly(Object, MediaType)}. */ + @Deprecated(since = "7.1", forRemoval = true) protected boolean supportsRepeatableWrites(T t) { - return false; + return canWriteRepeatedly(t, null); } diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractJacksonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractJacksonHttpMessageConverter.java index 40d6847b484..b9f730f0dcf 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/AbstractJacksonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractJacksonHttpMessageConverter.java @@ -285,6 +285,11 @@ public abstract class AbstractJacksonHttpMessageConverter hints) throws IOException, HttpMessageNotReadableException { @@ -194,6 +199,7 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter extends AbstractHttpM } @Override public boolean repeatable() { - return supportsRepeatableWrites(t); + return canWriteRepeatedly(t, contentType); } }); } diff --git a/spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java index b924827739e..b893b6a4972 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java @@ -50,6 +50,11 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter< return byte[].class == clazz; } + @Override + public boolean canWriteRepeatedly(byte[] bytes, @Nullable MediaType contentType) { + return true; + } + @Override public byte[] readInternal(Class clazz, HttpInputMessage message) throws IOException { long length = message.getHeaders().getContentLength(); @@ -68,6 +73,7 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter< } @Override + @SuppressWarnings("removal") protected boolean supportsRepeatableWrites(byte[] bytes) { return true; } diff --git a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java index fd3ad3012f1..193a5429949 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java @@ -486,7 +486,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter boolean checkPartsRepeatable(MultiValueMap map) { + private boolean checkPartsRepeatable(MultiValueMap map, MediaType contentType) { return map.entrySet().stream().allMatch(e -> e.getValue().stream().filter(Objects::nonNull).allMatch(part -> { HttpHeaders headers = null; Object body = part; @@ -515,9 +515,8 @@ public class FormHttpMessageConverter implements HttpMessageConverter converter = findConverterFor(e.getKey(), headers, body); - return (converter instanceof AbstractHttpMessageConverter ahmc && - ((AbstractHttpMessageConverter) ahmc).supportsRepeatableWrites((T) body)); + HttpMessageConverter converter = (HttpMessageConverter) findConverterFor(e.getKey(), headers, body); + return converter != null && converter.canWriteRepeatedly((T) body, contentType); })); } diff --git a/spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverter.java index f2419d004a6..b7adaeb3492 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverter.java @@ -108,4 +108,22 @@ public interface HttpMessageConverter { void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException; + /** + * Indicates whether this message converter can + * {@linkplain #write(Object, MediaType, HttpOutputMessage) write} the + * given payload multiple times. + *

This can be used by HTTP client libraries to know whether a message can be + * sent again, for example after an HTTP redirect. The default implementation + * returns {@code false}. This typically returns false if the payload can be read + * only once. + * @param t the object t + * @param contentType the content type to use when writing. + * @return {@code true} if {@code t} can be written repeatedly; + * {@code false} otherwise + * @since 7.1 + */ + default boolean canWriteRepeatedly(T t, @Nullable MediaType contentType) { + return false; + } + } diff --git a/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java index b305180831e..14a1cfc5bb4 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java @@ -99,6 +99,11 @@ public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConve return canWrite(mediaType) && this.conversionService.canConvert(clazz, String.class); } + @Override + public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) { + return true; + } + @Override protected boolean supports(Class clazz) { // should not be called, since we override canRead/Write @@ -137,6 +142,7 @@ public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConve } @Override + @SuppressWarnings("removal") protected boolean supportsRepeatableWrites(Object o) { return true; } diff --git a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java index 6365f10c3f0..0faea6dc8a5 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java @@ -71,6 +71,11 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter clazz) { return Resource.class.isAssignableFrom(clazz); @@ -141,8 +146,9 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter regions = (Collection) object; + for (ResourceRegion region : regions) { + if (!supportsRepeatableWrites(region)) { + return false; + } + } + return true; + } + } + @Override protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { @@ -140,20 +157,9 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa } @Override + @SuppressWarnings("removal") protected boolean supportsRepeatableWrites(Object object) { - if (object instanceof ResourceRegion resourceRegion) { - return supportsRepeatableWrites(resourceRegion); - } - else { - @SuppressWarnings("unchecked") - Collection regions = (Collection) object; - for (ResourceRegion region : regions) { - if (!supportsRepeatableWrites(region)) { - return false; - } - } - return true; - } + return canWriteRepeatedly(object, null); } private boolean supportsRepeatableWrites(ResourceRegion region) { diff --git a/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java index 5a9e81e90da..e280c0b4f10 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java @@ -89,6 +89,11 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter clazz, HttpInputMessage inputMessage) throws IOException { Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType()); @@ -161,6 +166,7 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter } + @Override + public boolean canWriteRepeatedly(T t, @Nullable MediaType contentType) { + return true; + } + @Override @SuppressWarnings("unchecked") protected T readInternal(Class clazz, HttpInputMessage inputMessage) @@ -108,6 +114,7 @@ public abstract class AbstractWireFeedHttpMessageConverter } @Override + @SuppressWarnings("removal") protected boolean supportsRepeatableWrites(T t) { return true; } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java index 62a8219bb45..22b07ac1928 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java @@ -292,6 +292,11 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener return false; } + @Override + public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) { + return true; + } + /** * Select an ObjectMapper to use, either the main ObjectMapper or another * if the handling for the given Class has been customized through @@ -570,6 +575,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener } @Override + @SuppressWarnings("removal") protected boolean supportsRepeatableWrites(Object o) { return true; } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java index 183a8afb388..b1f58ec8bd6 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java @@ -24,6 +24,7 @@ import java.lang.reflect.Type; import com.google.gson.Gson; import org.jspecify.annotations.Nullable; +import org.springframework.http.MediaType; import org.springframework.util.Assert; /** @@ -86,6 +87,10 @@ public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter { return this.gson; } + @Override + public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) { + return true; + } @Override protected Object readInternal(Type resolvedType, Reader reader) throws Exception { @@ -109,6 +114,7 @@ public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter { } @Override + @SuppressWarnings("removal") protected boolean supportsRepeatableWrites(Object o) { return true; } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java index 53300311cf2..5cc7e5ae98f 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java @@ -26,6 +26,7 @@ import jakarta.json.bind.JsonbBuilder; import jakarta.json.bind.JsonbConfig; import org.jspecify.annotations.Nullable; +import org.springframework.http.MediaType; import org.springframework.util.Assert; /** @@ -94,6 +95,10 @@ public class JsonbHttpMessageConverter extends AbstractJsonHttpMessageConverter return this.jsonb; } + @Override + public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) { + return true; + } @Override protected Object readInternal(Type resolvedType, Reader reader) throws Exception { @@ -111,6 +116,7 @@ public class JsonbHttpMessageConverter extends AbstractJsonHttpMessageConverter } @Override + @SuppressWarnings("removal") protected boolean supportsRepeatableWrites(Object o) { return true; } diff --git a/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java index 8f5eaacbe7c..8e4b12767e5 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java @@ -122,6 +122,12 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter clazz) { // should not be called, since we override canRead/Write @@ -235,6 +240,7 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa } @Override + @SuppressWarnings("removal") protected boolean supportsRepeatableWrites(Object o) { return true; } diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java index 459436a4fcf..0c383e5b958 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java @@ -114,6 +114,11 @@ public class MarshallingHttpMessageConverter extends AbstractXmlHttpMessageConve return (canWrite(mediaType) && this.marshaller != null && this.marshaller.supports(clazz)); } + @Override + public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) { + return true; + } + @Override protected boolean supports(Class clazz) { // should not be called, since we override canRead()/canWrite() @@ -137,6 +142,7 @@ public class MarshallingHttpMessageConverter extends AbstractXmlHttpMessageConve } @Override + @SuppressWarnings("removal") protected boolean supportsRepeatableWrites(Object o) { return true; } diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java index 3cc4cb533e4..80ea8de6acc 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java @@ -148,6 +148,11 @@ public class SourceHttpMessageConverter extends AbstractHttpMe return SUPPORTED_CLASSES.contains(clazz); } + @Override + public boolean canWriteRepeatedly(T t, @Nullable MediaType contentType) { + return t instanceof DOMSource; + } + @Override @SuppressWarnings("unchecked") protected T readInternal(Class clazz, HttpInputMessage inputMessage) @@ -293,8 +298,9 @@ public class SourceHttpMessageConverter extends AbstractHttpMe } @Override + @SuppressWarnings("removal") protected boolean supportsRepeatableWrites(T t) { - return t instanceof DOMSource; + return canWriteRepeatedly(t, null); }