Skip to content

Commit abe3cfd

Browse files
committed
Flux<String> + "application/json" renders as text
Spring MVC now treats Flux<String> + "application/json" as (serialized) text to be written directly to the response as is. This is consistent with the rendering of String + "application/json". Issue: SPR-15456
1 parent c67b0d6 commit abe3cfd

File tree

2 files changed

+79
-94
lines changed

2 files changed

+79
-94
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandler.java

+5-11
Original file line numberDiff line numberDiff line change
@@ -131,16 +131,16 @@ public ResponseBodyEmitter handleValue(Object returnValue, MethodParameter retur
131131
new SseEmitterSubscriber(emitter, this.taskExecutor).connect(adapter, returnValue);
132132
return emitter;
133133
}
134+
if (CharSequence.class.isAssignableFrom(elementClass)) {
135+
ResponseBodyEmitter emitter = getEmitter(mediaType.orElse(MediaType.TEXT_PLAIN));
136+
new TextEmitterSubscriber(emitter, this.taskExecutor).connect(adapter, returnValue);
137+
return emitter;
138+
}
134139
if (mediaTypes.stream().anyMatch(MediaType.APPLICATION_STREAM_JSON::includes)) {
135140
ResponseBodyEmitter emitter = getEmitter(MediaType.APPLICATION_STREAM_JSON);
136141
new JsonEmitterSubscriber(emitter, this.taskExecutor).connect(adapter, returnValue);
137142
return emitter;
138143
}
139-
if (CharSequence.class.isAssignableFrom(elementClass) && !isJsonStringArray(elementClass, mediaType)) {
140-
ResponseBodyEmitter emitter = getEmitter(mediaType.orElse(MediaType.TEXT_PLAIN));
141-
new TextEmitterSubscriber(emitter, this.taskExecutor).connect(adapter, returnValue);
142-
return emitter;
143-
}
144144
}
145145

146146
// Not streaming...
@@ -162,12 +162,6 @@ private Collection<MediaType> getMediaTypes(NativeWebRequest request)
162162
this.contentNegotiationManager.resolveMediaTypes(request) : mediaTypes;
163163
}
164164

165-
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
166-
private boolean isJsonStringArray(Class<?> elementType, Optional<MediaType> mediaType) {
167-
return CharSequence.class.isAssignableFrom(elementType) && mediaType.filter(type ->
168-
MediaType.APPLICATION_JSON.includes(type) || JSON_TYPE.includes(type)).isPresent();
169-
}
170-
171165
private ResponseBodyEmitter getEmitter(MediaType mediaType) {
172166
return new ResponseBodyEmitter() {
173167

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandlerTests.java

+74-83
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
package org.springframework.web.servlet.mvc.method.annotation;
1717

1818
import java.io.IOException;
19+
import java.util.ArrayList;
1920
import java.util.Arrays;
2021
import java.util.Collections;
22+
import java.util.List;
2123
import java.util.Set;
2224
import java.util.concurrent.atomic.AtomicReference;
25+
import java.util.stream.Collectors;
2326

2427
import org.junit.Before;
2528
import org.junit.Test;
@@ -52,8 +55,8 @@
5255
import static junit.framework.TestCase.assertNull;
5356
import static org.junit.Assert.assertEquals;
5457
import static org.junit.Assert.assertFalse;
55-
import static org.junit.Assert.assertNotNull;
5658
import static org.junit.Assert.assertTrue;
59+
import static org.springframework.core.ResolvableType.forClass;
5760
import static org.springframework.web.method.ResolvableMethod.on;
5861

5962
/**
@@ -108,27 +111,27 @@ public void deferredResultSubscriberWithOneValue() throws Exception {
108111

109112
// Mono
110113
MonoProcessor<String> mono = MonoProcessor.create();
111-
testDeferredResultSubscriber(mono, Mono.class, () -> mono.onNext("foo"), "foo");
114+
testDeferredResultSubscriber(mono, Mono.class, forClass(String.class), () -> mono.onNext("foo"), "foo");
112115

113116
// Mono empty
114117
MonoProcessor<String> monoEmpty = MonoProcessor.create();
115-
testDeferredResultSubscriber(monoEmpty, Mono.class, monoEmpty::onComplete, null);
118+
testDeferredResultSubscriber(monoEmpty, Mono.class, forClass(String.class), monoEmpty::onComplete, null);
116119

117120
// RxJava 1 Single
118121
AtomicReference<SingleEmitter<String>> ref = new AtomicReference<>();
119122
Single<String> single = Single.fromEmitter(ref::set);
120-
testDeferredResultSubscriber(single, Single.class, () -> ref.get().onSuccess("foo"), "foo");
123+
testDeferredResultSubscriber(single, Single.class, forClass(String.class), () -> ref.get().onSuccess("foo"), "foo");
121124

122125
// RxJava 2 Single
123126
AtomicReference<io.reactivex.SingleEmitter<String>> ref2 = new AtomicReference<>();
124127
io.reactivex.Single<String> single2 = io.reactivex.Single.create(ref2::set);
125-
testDeferredResultSubscriber(single2, io.reactivex.Single.class, () -> ref2.get().onSuccess("foo"), "foo");
128+
testDeferredResultSubscriber(single2, io.reactivex.Single.class, forClass(String.class), () -> ref2.get().onSuccess("foo"), "foo");
126129
}
127130

128131
@Test
129132
public void deferredResultSubscriberWithNoValues() throws Exception {
130133
MonoProcessor<String> monoEmpty = MonoProcessor.create();
131-
testDeferredResultSubscriber(monoEmpty, Mono.class, monoEmpty::onComplete, null);
134+
testDeferredResultSubscriber(monoEmpty, Mono.class, forClass(String.class), monoEmpty::onComplete, null);
132135
}
133136

134137
@Test
@@ -137,13 +140,15 @@ public void deferredResultSubscriberWithMultipleValues() throws Exception {
137140
// JSON must be preferred for Flux<String> -> List<String> or else we stream
138141
this.servletRequest.addHeader("Accept", "application/json");
139142

140-
EmitterProcessor<String> emitter = EmitterProcessor.create();
141-
testDeferredResultSubscriber(emitter, Flux.class, () -> {
142-
emitter.onNext("foo");
143-
emitter.onNext("bar");
144-
emitter.onNext("baz");
143+
Bar bar1 = new Bar("foo");
144+
Bar bar2 = new Bar("bar");
145+
146+
EmitterProcessor<Bar> emitter = EmitterProcessor.create();
147+
testDeferredResultSubscriber(emitter, Flux.class, forClass(Bar.class), () -> {
148+
emitter.onNext(bar1);
149+
emitter.onNext(bar2);
145150
emitter.onComplete();
146-
}, Arrays.asList("foo", "bar", "baz"));
151+
}, Arrays.asList(bar1, bar2));
147152
}
148153

149154
@Test
@@ -153,48 +158,17 @@ public void deferredResultSubscriberWithError() throws Exception {
153158

154159
// Mono
155160
MonoProcessor<String> mono = MonoProcessor.create();
156-
testDeferredResultSubscriber(mono, Mono.class, () -> mono.onError(ex), ex);
161+
testDeferredResultSubscriber(mono, Mono.class, forClass(String.class), () -> mono.onError(ex), ex);
157162

158163
// RxJava 1 Single
159164
AtomicReference<SingleEmitter<String>> ref = new AtomicReference<>();
160165
Single<String> single = Single.fromEmitter(ref::set);
161-
testDeferredResultSubscriber(single, Single.class, () -> ref.get().onError(ex), ex);
166+
testDeferredResultSubscriber(single, Single.class, forClass(String.class), () -> ref.get().onError(ex), ex);
162167

163168
// RxJava 2 Single
164169
AtomicReference<io.reactivex.SingleEmitter<String>> ref2 = new AtomicReference<>();
165170
io.reactivex.Single<String> single2 = io.reactivex.Single.create(ref2::set);
166-
testDeferredResultSubscriber(single2, io.reactivex.Single.class, () -> ref2.get().onError(ex), ex);
167-
}
168-
169-
@Test
170-
public void jsonArrayOfStrings() throws Exception {
171-
172-
// Empty -> null
173-
testJsonNotPreferred("text/plain");
174-
testJsonNotPreferred("text/plain, application/json");
175-
testJsonNotPreferred("text/markdown");
176-
testJsonNotPreferred("foo/bar");
177-
178-
// Empty -> List[0] when JSON is preferred
179-
testJsonPreferred("application/json");
180-
testJsonPreferred("application/foo+json");
181-
testJsonPreferred("application/json, text/plain");
182-
testJsonPreferred("*/*, application/json, text/plain");
183-
}
184-
185-
private void testJsonNotPreferred(String acceptHeaderValue) throws Exception {
186-
resetRequest();
187-
this.servletRequest.addHeader("Accept", acceptHeaderValue);
188-
EmitterProcessor<String> processor = EmitterProcessor.create();
189-
ResponseBodyEmitter emitter = handleValue(processor, Flux.class);
190-
assertNotNull(emitter);
191-
}
192-
193-
private void testJsonPreferred(String acceptHeaderValue) throws Exception {
194-
resetRequest();
195-
this.servletRequest.addHeader("Accept", acceptHeaderValue);
196-
EmitterProcessor<String> processor = EmitterProcessor.create();
197-
testDeferredResultSubscriber(processor, Flux.class, processor::onComplete, Collections.emptyList());
171+
testDeferredResultSubscriber(single2, io.reactivex.Single.class, forClass(String.class), () -> ref2.get().onError(ex), ex);
198172
}
199173

200174
@Test
@@ -211,14 +185,10 @@ public void mediaTypes() throws Exception {
211185

212186
// No media type preferences
213187
testSseResponse(false);
214-
215-
// Requested media types are sorted
216-
testJsonPreferred("text/plain;q=0.8, application/json;q=1.0");
217-
testJsonNotPreferred("text/plain, application/json");
218188
}
219189

220190
private void testSseResponse(boolean expectSseEimtter) throws Exception {
221-
ResponseBodyEmitter emitter = handleValue(Flux.empty(), Flux.class);
191+
ResponseBodyEmitter emitter = handleValue(Flux.empty(), Flux.class, forClass(String.class));
222192
assertEquals(expectSseEimtter, emitter instanceof SseEmitter);
223193
resetRequest();
224194
}
@@ -228,7 +198,7 @@ public void writeServerSentEvents() throws Exception {
228198

229199
this.servletRequest.addHeader("Accept", "text/event-stream");
230200
EmitterProcessor<String> processor = EmitterProcessor.create();
231-
SseEmitter sseEmitter = (SseEmitter) handleValue(processor, Flux.class);
201+
SseEmitter sseEmitter = (SseEmitter) handleValue(processor, Flux.class, forClass(String.class));
232202

233203
EmitterHandler emitterHandler = new EmitterHandler();
234204
sseEmitter.initialize(emitterHandler);
@@ -238,11 +208,11 @@ public void writeServerSentEvents() throws Exception {
238208
processor.onNext("baz");
239209
processor.onComplete();
240210

241-
assertEquals("data:foo\n\ndata:bar\n\ndata:baz\n\n", emitterHandler.getOutput());
211+
assertEquals("data:foo\n\ndata:bar\n\ndata:baz\n\n", emitterHandler.getValuesAsText());
242212
}
243213

244214
@Test
245-
public void writeSentEventsWithBuilder() throws Exception {
215+
public void writeServerSentEventsWithBuilder() throws Exception {
246216

247217
ResolvableType type = ResolvableType.forClassWithGenerics(ServerSentEvent.class, String.class);
248218

@@ -258,36 +228,39 @@ public void writeSentEventsWithBuilder() throws Exception {
258228
processor.onComplete();
259229

260230
assertEquals("id:1\ndata:foo\n\nid:2\ndata:bar\n\nid:3\ndata:baz\n\n",
261-
emitterHandler.getOutput());
231+
emitterHandler.getValuesAsText());
262232
}
263233

264234
@Test
265235
public void writeStreamJson() throws Exception {
266236

267237
this.servletRequest.addHeader("Accept", "application/stream+json");
268238

269-
EmitterProcessor<String> processor = EmitterProcessor.create();
270-
ResponseBodyEmitter emitter = handleValue(processor, Flux.class);
239+
EmitterProcessor<Bar> processor = EmitterProcessor.create();
240+
ResponseBodyEmitter emitter = handleValue(processor, Flux.class, forClass(Bar.class));
271241

272242
EmitterHandler emitterHandler = new EmitterHandler();
273243
emitter.initialize(emitterHandler);
274244

275245
ServletServerHttpResponse message = new ServletServerHttpResponse(this.servletResponse);
276246
emitter.extendResponse(message);
277247

278-
processor.onNext("[\"foo\",\"bar\"]");
279-
processor.onNext("[\"bar\",\"baz\"]");
248+
Bar bar1 = new Bar("foo");
249+
Bar bar2 = new Bar("bar");
250+
251+
processor.onNext(bar1);
252+
processor.onNext(bar2);
280253
processor.onComplete();
281254

282255
assertEquals("application/stream+json", message.getHeaders().getContentType().toString());
283-
assertEquals("[\"foo\",\"bar\"]\n[\"bar\",\"baz\"]\n", emitterHandler.getOutput());
256+
assertEquals(Arrays.asList(bar1, "\n", bar2, "\n"), emitterHandler.getValues());
284257
}
285258

286259
@Test
287260
public void writeText() throws Exception {
288261

289262
EmitterProcessor<String> processor = EmitterProcessor.create();
290-
ResponseBodyEmitter emitter = handleValue(processor, Flux.class);
263+
ResponseBodyEmitter emitter = handleValue(processor, Flux.class, forClass(String.class));
291264

292265
EmitterHandler emitterHandler = new EmitterHandler();
293266
emitter.initialize(emitterHandler);
@@ -297,41 +270,45 @@ public void writeText() throws Exception {
297270
processor.onNext("the lazy dog");
298271
processor.onComplete();
299272

300-
assertEquals("The quick brown fox jumps over the lazy dog", emitterHandler.getOutput());
273+
assertEquals("The quick brown fox jumps over the lazy dog", emitterHandler.getValuesAsText());
301274
}
302275

303276
@Test
304-
public void writeTextContentType() throws Exception {
277+
public void writeFluxOfString() throws Exception {
278+
279+
// Default to "text/plain"
280+
testEmitterContentType("text/plain");
305281

306-
// Any requested, concrete, "text" media type
282+
// Same if no concrete media type
283+
this.servletRequest.addHeader("Accept", "text/*");
284+
testEmitterContentType("text/plain");
285+
286+
// Otherwise pick concrete media type
307287
this.servletRequest.addHeader("Accept", "*/*, text/*, text/markdown");
308288
testEmitterContentType("text/markdown");
309289

310-
// Or any requested concrete media type
290+
// Any concrete media type
311291
this.servletRequest.addHeader("Accept", "*/*, text/*, foo/bar");
312292
testEmitterContentType("foo/bar");
313293

314-
// Or default to...
315-
testEmitterContentType("text/plain");
316-
317-
// Or default to if not concrete..
318-
this.servletRequest.addHeader("Accept", "text/*");
319-
testEmitterContentType("text/plain");
294+
// Including json
295+
this.servletRequest.addHeader("Accept", "*/*, text/*, application/json");
296+
testEmitterContentType("application/json");
320297
}
321298

322299
private void testEmitterContentType(String expected) throws Exception {
323300
ServletServerHttpResponse message = new ServletServerHttpResponse(this.servletResponse);
324-
ResponseBodyEmitter emitter = handleValue(Flux.empty(), Flux.class);
301+
ResponseBodyEmitter emitter = handleValue(Flux.empty(), Flux.class, forClass(String.class));
325302
emitter.extendResponse(message);
326303
assertEquals(expected, message.getHeaders().getContentType().toString());
327304
resetRequest();
328305
}
329306

330307

331308
private void testDeferredResultSubscriber(Object returnValue, Class<?> asyncType,
332-
Runnable produceTask, Object expected) throws Exception {
309+
ResolvableType elementType, Runnable produceTask, Object expected) throws Exception {
333310

334-
ResponseBodyEmitter emitter = handleValue(returnValue, asyncType);
311+
ResponseBodyEmitter emitter = handleValue(returnValue, asyncType, elementType);
335312
assertNull(emitter);
336313

337314
assertTrue(this.servletRequest.isAsyncStarted());
@@ -345,10 +322,6 @@ private void testDeferredResultSubscriber(Object returnValue, Class<?> asyncType
345322
resetRequest();
346323
}
347324

348-
private ResponseBodyEmitter handleValue(Object returnValue, Class<?> asyncType) throws Exception {
349-
return handleValue(returnValue, asyncType, ResolvableType.forClass(String.class));
350-
}
351-
352325
private ResponseBodyEmitter handleValue(Object returnValue, Class<?> asyncType,
353326
ResolvableType genericType) throws Exception {
354327

@@ -369,24 +342,30 @@ static class TestController {
369342

370343
io.reactivex.Single<String> handleSingleRxJava2() { return null; }
371344

372-
Flux<String> handleFlux() { return null; }
345+
Flux<Bar> handleFlux() { return null; }
346+
347+
Flux<String> handleFluxString() { return null; }
373348

374349
Flux<ServerSentEvent<String>> handleFluxSseEventBuilder() { return null; }
375350
}
376351

377352

378353
private static class EmitterHandler implements ResponseBodyEmitter.Handler {
379354

380-
private final StringBuilder stringBuilder = new StringBuilder();
355+
private final List<Object> values = new ArrayList<>();
381356

382357

383-
public String getOutput() {
384-
return this.stringBuilder.toString();
358+
public List<?> getValues() {
359+
return this.values;
360+
}
361+
362+
public String getValuesAsText() {
363+
return this.values.stream().map(Object::toString).collect(Collectors.joining());
385364
}
386365

387366
@Override
388367
public void send(Object data, MediaType mediaType) throws IOException {
389-
this.stringBuilder.append(data);
368+
this.values.add(data);
390369
}
391370

392371
@Override
@@ -406,4 +385,16 @@ public void onCompletion(Runnable callback) {
406385
}
407386
}
408387

388+
private static class Bar {
389+
390+
private final String value;
391+
392+
public Bar(String value) {
393+
this.value = value;
394+
}
395+
396+
public String getValue() {
397+
return this.value;
398+
}
399+
}
409400
}

0 commit comments

Comments
 (0)