diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 5147d2c59c6..9c11846c9c3 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -603,7 +603,7 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp finally { DataBufferUtils.release(buffer); } - text = text.replace("\n", "\ndata:"); + text = escapeSseFragment(text); return bufferFactory.wrap(text.getBytes(charset)); }); @@ -614,6 +614,30 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp byte[] bytes = text.getBytes(charset); return bufferFactory.wrap(bytes); } + + private String escapeSseFragment(String content) { + if (content.indexOf('\n') == -1 && content.indexOf('\r') == -1) { + return content; + } + StringBuilder fragment = new StringBuilder(); + int length = content.length(); + for (int i = 0; i < length; i++) { + char c = content.charAt(i); + if (c == '\r') { + if (i + 1 < length && content.charAt(i + 1) == '\n') { + i++; + } + fragment.append("\ndata:"); + } + else if (c == '\n') { + fragment.append("\ndata:"); + } + else { + fragment.append(c); + } + } + return fragment.toString(); + } } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java index 7a9ff80abbf..db95230fc13 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java @@ -119,6 +119,21 @@ class FragmentViewResolutionResultHandlerTests { """); } + @Test + void escapeViewFragment() { + Fragment fragment = Fragment.create("fragment1", Map.of("foo", "Foo\n and Bar")); + testSse(Flux.just(fragment), + on(Handler.class).resolveReturnType(Flux.class, Fragment.class), + """ + event:fragment1 + data:
+ data: Hello Foo + data: and Bar + data:
+ + """); + } + @Test void renderServerSentEventFragmentStream() { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java index dd038afa785..1c5cd5dd2a3 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java @@ -476,8 +476,27 @@ public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodRetur public byte[] getFragmentContent() { this.writer.flush(); String content = this.outputStream.toString(this.charset); - content = content.replace("\n", "\ndata:"); - return content.getBytes(this.charset); + if (content.indexOf('\n') == -1 && content.indexOf('\r') == -1) { + return content.getBytes(this.charset); + } + StringBuilder fragment = new StringBuilder(); + int length = content.length(); + for (int i = 0; i < length; i++) { + char c = content.charAt(i); + if (c == '\r') { + if (i + 1 < length && content.charAt(i + 1) == '\n') { + i++; + } + fragment.append("\ndata:"); + } + else if (c == '\n') { + fragment.append("\ndata:"); + } + else { + fragment.append(c); + } + } + return fragment.toString().getBytes(this.charset); } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/FragmentRenderingStreamTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/FragmentRenderingStreamTests.java index a5fec8bd397..f478dd9dfa9 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/FragmentRenderingStreamTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/FragmentRenderingStreamTests.java @@ -146,6 +146,31 @@ class FragmentRenderingStreamTests { """)); } + @Test + void escapeViewFragment() throws Exception { + MethodParameter type = on(TestController.class).resolveReturnType(SseEmitter.class); + + SseEmitter emitter = new SseEmitter(); + this.handler.handleReturnValue(emitter, type, new ModelAndViewContainer(), webRequest); + + assertThat(this.request.isAsyncStarted()).isTrue(); + assertThat(this.response.getStatus()).isEqualTo(200); + + ModelAndView mav1 = new ModelAndView("fragment1", Map.of("foo", "Foo\n and Bar")); + + emitter.send(SseEmitter.event().data(mav1)); + + assertThat(this.response.getContentType()).isEqualTo("text/event-stream"); + assertThat(this.response.getContentAsString()).isEqualTo((""" + event:fragment1 + data:+ data: Hello Foo + data: and Bar + data:
+ + """)); + } + @SuppressWarnings({"unused", "DataFlowIssue"}) private static class TestController {