Skip to content

Commit 6dd93d4

Browse files
committed
Allow repeatable writes in HttpMessageConverter
This commit ensures that the StreamingHttpOutputMessage.Body.repeatable flag is set in message converters for bodies that can be written repeatedly. Closes gh-31516 See gh-31449
1 parent ab316d9 commit 6dd93d4

22 files changed

+206
-25
lines changed

spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -88,16 +88,27 @@ public final void write(final T t, @Nullable final Type type, @Nullable MediaTyp
8888
addDefaultHeaders(headers, t, contentType);
8989

9090
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
91-
streamingOutputMessage.setBody(outputStream -> writeInternal(t, type, new HttpOutputMessage() {
91+
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
9292
@Override
93-
public OutputStream getBody() {
94-
return outputStream;
93+
public void writeTo(OutputStream outputStream) throws IOException {
94+
writeInternal(t, type, new HttpOutputMessage() {
95+
@Override
96+
public OutputStream getBody() {
97+
return outputStream;
98+
}
99+
100+
@Override
101+
public HttpHeaders getHeaders() {
102+
return headers;
103+
}
104+
});
95105
}
106+
96107
@Override
97-
public HttpHeaders getHeaders() {
98-
return headers;
108+
public boolean repeatable() {
109+
return supportsRepeatableWrites(t);
99110
}
100-
}));
111+
});
101112
}
102113
else {
103114
writeInternal(t, type, outputMessage);

spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,16 +210,27 @@ public final void write(final T t, @Nullable MediaType contentType, HttpOutputMe
210210
addDefaultHeaders(headers, t, contentType);
211211

212212
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
213-
streamingOutputMessage.setBody(outputStream -> writeInternal(t, new HttpOutputMessage() {
213+
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
214214
@Override
215-
public OutputStream getBody() {
216-
return outputStream;
215+
public void writeTo(OutputStream outputStream) throws IOException {
216+
writeInternal(t, new HttpOutputMessage() {
217+
@Override
218+
public OutputStream getBody() {
219+
return outputStream;
220+
}
221+
222+
@Override
223+
public HttpHeaders getHeaders() {
224+
return headers;
225+
}
226+
});
217227
}
228+
218229
@Override
219-
public HttpHeaders getHeaders() {
220-
return headers;
230+
public boolean repeatable() {
231+
return supportsRepeatableWrites(t);
221232
}
222-
}));
233+
});
223234
}
224235
else {
225236
writeInternal(t, outputMessage);
@@ -289,6 +300,21 @@ protected Long getContentLength(T t, @Nullable MediaType contentType) throws IOE
289300
return null;
290301
}
291302

303+
/**
304+
* Indicates whether this message converter can
305+
* {@linkplain #write(Object, MediaType, HttpOutputMessage) write} the
306+
* given object multiple times.
307+
*
308+
* <p>Default implementation returns {@code false}.
309+
* @param t the object t
310+
* @return {@code true} if {@code t} can be written repeatedly;
311+
* {@code false} otherwise
312+
* @since 6.1
313+
*/
314+
protected boolean supportsRepeatableWrites(T t) {
315+
return false;
316+
}
317+
292318

293319
/**
294320
* Indicates whether the given class is supported by this converter.

spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,9 @@ private boolean hasPolymorphism(SerialDescriptor descriptor, Set<String> already
178178
}
179179
return false;
180180
}
181+
182+
@Override
183+
protected boolean supportsRepeatableWrites(Object object) {
184+
return true;
185+
}
181186
}

spring-web/src/main/java/org/springframework/http/converter/BufferedImageHttpMessageConverter.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -226,7 +226,17 @@ public void write(final BufferedImage image, @Nullable final MediaType contentTy
226226
outputMessage.getHeaders().setContentType(selectedContentType);
227227

228228
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
229-
streamingOutputMessage.setBody(outputStream -> writeInternal(image, selectedContentType, outputStream));
229+
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
230+
@Override
231+
public void writeTo(OutputStream outputStream) throws IOException {
232+
BufferedImageHttpMessageConverter.this.writeInternal(image, selectedContentType, outputStream);
233+
}
234+
235+
@Override
236+
public boolean repeatable() {
237+
return true;
238+
}
239+
});
230240
}
231241
else {
232242
writeInternal(image, selectedContentType, outputMessage.getBody());

spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,8 @@ protected void writeInternal(byte[] bytes, HttpOutputMessage outputMessage) thro
6767
StreamUtils.copy(bytes, outputMessage.getBody());
6868
}
6969

70+
@Override
71+
protected boolean supportsRepeatableWrites(byte[] bytes) {
72+
return true;
73+
}
7074
}

spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,17 @@ private void writeForm(MultiValueMap<String, Object> formData, @Nullable MediaTy
400400
outputMessage.getHeaders().setContentLength(bytes.length);
401401

402402
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
403-
streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(bytes, outputStream));
403+
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
404+
@Override
405+
public void writeTo(OutputStream outputStream) throws IOException {
406+
StreamUtils.copy(bytes, outputStream);
407+
}
408+
409+
@Override
410+
public boolean repeatable() {
411+
return true;
412+
}
413+
});
404414
}
405415
else {
406416
StreamUtils.copy(bytes, outputMessage.getBody());

spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -135,4 +135,8 @@ protected Long getContentLength(Object obj, @Nullable MediaType contentType) {
135135
return this.stringHttpMessageConverter.getContentLength(value, contentType);
136136
}
137137

138+
@Override
139+
protected boolean supportsRepeatableWrites(Object o) {
140+
return true;
141+
}
138142
}

spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -166,4 +166,8 @@ protected void writeContent(Resource resource, HttpOutputMessage outputMessage)
166166
}
167167
}
168168

169+
@Override
170+
protected boolean supportsRepeatableWrites(Resource resource) {
171+
return !(resource instanceof InputStreamResource);
172+
}
169173
}

spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@
2424
import java.nio.charset.StandardCharsets;
2525
import java.util.Collection;
2626

27+
import org.springframework.core.io.InputStreamResource;
2728
import org.springframework.core.io.Resource;
2829
import org.springframework.core.io.support.ResourceRegion;
2930
import org.springframework.http.HttpHeaders;
@@ -238,4 +239,24 @@ private static void print(OutputStream os, String buf) throws IOException {
238239
os.write(buf.getBytes(StandardCharsets.US_ASCII));
239240
}
240241

242+
@Override
243+
@SuppressWarnings("unchecked")
244+
protected boolean supportsRepeatableWrites(Object object) {
245+
if (object instanceof ResourceRegion resourceRegion) {
246+
return supportsRepeatableWrites(resourceRegion);
247+
}
248+
else {
249+
Collection<ResourceRegion> regions = (Collection<ResourceRegion>) object;
250+
for (ResourceRegion region : regions) {
251+
if (!supportsRepeatableWrites(region)) {
252+
return false;
253+
}
254+
}
255+
return true;
256+
}
257+
}
258+
259+
private boolean supportsRepeatableWrites(ResourceRegion region) {
260+
return !(region.getResource() instanceof InputStreamResource);
261+
}
241262
}

spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,8 @@ else if (contentType.isCompatibleWith(MediaType.APPLICATION_JSON) ||
163163
return charset;
164164
}
165165

166+
@Override
167+
protected boolean supportsRepeatableWrites(String s) {
168+
return true;
169+
}
166170
}

0 commit comments

Comments
 (0)