Skip to content

Commit 53cadf1

Browse files
committed
Deferred handling of Flux error in Spring MVC
This commit defers flushing of the response until the first item is emitted that needs to be written (and flushed) to the response. This makes Spring MVC consistent with WebFlux in this regard. Closes gh-21972
1 parent 15e1af2 commit 53cadf1

File tree

2 files changed

+37
-9
lines changed

2 files changed

+37
-9
lines changed

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,8 @@ public void handleReturnValue(@Nullable Object returnValue, MethodParameter retu
152152
// At this point we know we're streaming..
153153
ShallowEtagHeaderFilter.disableContentCaching(request);
154154

155-
// Commit the response and wrap to ignore further header changes
156-
outputMessage.getBody();
157-
outputMessage.flush();
155+
// Wrap the response to ignore further header changes
156+
// Headers will be flushed at the first write
158157
outputMessage = new StreamingServletServerHttpResponse(outputMessage);
159158

160159
DeferredResult<?> deferredResult = new DeferredResult<>(emitter.getTimeout());
@@ -198,7 +197,13 @@ private <T> void sendInternal(T data, @Nullable MediaType mediaType) throws IOEx
198197

199198
@Override
200199
public void complete() {
201-
this.deferredResult.setResult(null);
200+
try {
201+
this.outputMessage.flush();
202+
this.deferredResult.setResult(null);
203+
}
204+
catch (IOException ex) {
205+
this.deferredResult.setErrorResult(ex);
206+
}
202207
}
203208

204209
@Override

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

Lines changed: 28 additions & 5 deletions
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-2019 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.
@@ -40,6 +40,7 @@
4040
import org.springframework.web.context.request.ServletWebRequest;
4141
import org.springframework.web.context.request.async.AsyncWebRequest;
4242
import org.springframework.web.context.request.async.StandardServletAsyncWebRequest;
43+
import org.springframework.web.context.request.async.WebAsyncManager;
4344
import org.springframework.web.context.request.async.WebAsyncUtils;
4445
import org.springframework.web.method.support.ModelAndViewContainer;
4546

@@ -197,7 +198,6 @@ public void sseEmitter() throws Exception {
197198

198199
assertTrue(this.request.isAsyncStarted());
199200
assertEquals(200, this.response.getStatus());
200-
assertEquals("text/event-stream;charset=UTF-8", this.response.getContentType());
201201

202202
SimpleBean bean1 = new SimpleBean();
203203
bean1.setId(1L);
@@ -210,6 +210,7 @@ public void sseEmitter() throws Exception {
210210
emitter.send(SseEmitter.event().
211211
comment("a test").name("update").id("1").reconnectTime(5000L).data(bean1).data(bean2));
212212

213+
assertEquals("text/event-stream;charset=UTF-8", this.response.getContentType());
213214
assertEquals(":a test\n" +
214215
"event:update\n" +
215216
"id:1\n" +
@@ -230,21 +231,43 @@ public void responseBodyFlux() throws Exception {
230231

231232
assertTrue(this.request.isAsyncStarted());
232233
assertEquals(200, this.response.getStatus());
233-
assertEquals("text/event-stream;charset=UTF-8", this.response.getContentType());
234234

235235
processor.onNext("foo");
236236
processor.onNext("bar");
237237
processor.onNext("baz");
238238
processor.onComplete();
239239

240+
assertEquals("text/event-stream;charset=UTF-8", this.response.getContentType());
240241
assertEquals("data:foo\n\ndata:bar\n\ndata:baz\n\n", this.response.getContentAsString());
241242
}
242243

244+
@Test // gh-21972
245+
public void responseBodyFluxWithError() throws Exception {
246+
247+
this.request.addHeader("Accept", "text/event-stream");
248+
249+
MethodParameter type = on(TestController.class).resolveReturnType(Flux.class, String.class);
250+
EmitterProcessor<String> processor = EmitterProcessor.create();
251+
this.handler.handleReturnValue(processor, type, this.mavContainer, this.webRequest);
252+
253+
assertTrue(this.request.isAsyncStarted());
254+
255+
IllegalStateException ex = new IllegalStateException("wah wah");
256+
processor.onError(ex);
257+
processor.onComplete();
258+
259+
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(this.webRequest);
260+
assertSame(ex, asyncManager.getConcurrentResult());
261+
assertNull(this.response.getContentType());
262+
}
263+
243264
@Test
244265
public void responseEntitySse() throws Exception {
245266
MethodParameter type = on(TestController.class).resolveReturnType(ResponseEntity.class, SseEmitter.class);
246-
ResponseEntity<SseEmitter> entity = ResponseEntity.ok().header("foo", "bar").body(new SseEmitter());
267+
SseEmitter emitter = new SseEmitter();
268+
ResponseEntity<SseEmitter> entity = ResponseEntity.ok().header("foo", "bar").body(emitter);
247269
this.handler.handleReturnValue(entity, type, this.mavContainer, this.webRequest);
270+
emitter.complete();
248271

249272
assertTrue(this.request.isAsyncStarted());
250273
assertEquals(200, this.response.getStatus());
@@ -274,13 +297,13 @@ public void responseEntityFlux() throws Exception {
274297

275298
assertTrue(this.request.isAsyncStarted());
276299
assertEquals(200, this.response.getStatus());
277-
assertEquals("text/plain", this.response.getContentType());
278300

279301
processor.onNext("foo");
280302
processor.onNext("bar");
281303
processor.onNext("baz");
282304
processor.onComplete();
283305

306+
assertEquals("text/plain", this.response.getContentType());
284307
assertEquals("foobarbaz", this.response.getContentAsString());
285308
}
286309

0 commit comments

Comments
 (0)